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

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


  • 首页

  • 归档

  • 搜索

springcloud Eureka上 笔记 0 环境 1 

发表于 2021-11-25

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

0 环境

1
2
复制代码系统环境:win10
编辑器:IDEA

1 注册中心

1
2
3
4
scss复制代码Eureka是springcloud中的注册中心。
原因:
当是单体应用 类似一条直线
随着项目越来越大 系统拆分 类似那个藕(模块间相互协作 调用) 为了解决服务之间的耦合 注册中心有必要出现了

2 Eureka构成

1
2
3
4
5
6
arduino复制代码Eureka分为两部分:服务端和客户端,服务端就是注册中心,接收其他服务注册,客户端则是Java客户端,用来注册。

Eureka有三个角色:
Eureka Server:注册中心
Eureka Provider:服务提供者
Eureka Consumer:服务消费者

3 基础搭建

1
复制代码Eureka由Java开发的,springcloud使用springboot对Eureka进行了封装,使用方便。

1 对应的版本

2 创建一个springboot项目 添加Eureka依赖

2 yml配置 启动Eureka功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yaml复制代码# 给当前服务取一个名字
spring:
application:
name: eureka

# 设置端口号
server:
port: 1234

# 默认情况下 Eureka Server也是一个普通的微服务 即当它是一个注册中心的时候 他有2种 1:注册中心 2:普通服务(把自己注册到自己的上面去)
# 比喻不形象 Server是一个结婚登记处 1:他是一个登记员 登记其他人的结婚信息 2:同时他也是人类的一员 他也可能结婚 那么自己登记自己老 也不是不可以

# register-with-eureka为false 表示不把当前项目注册到服务中心上
# fetch-registry为false 表示不从Eureka Server上获取注册信息
eureka:
client:
register-with-eureka: false
fetch-registry: false

3 启动项目

浏览器输入http://localhost:1234/

4 Eureka后台管理界面介绍

4 Eureka集群

1
2
3
4
scss复制代码注册中心使用之后 所有服务都要通过服务注册中心进行信息交换 服务注册中心的稳定性很重要 一旦注册中心崩了 整个系统都会受到影响 在实际使用中 Eureka一般是以集群的形式出现的

Eureka集群 多个Eureka实例的启动 多Eureka实例之间 相互注册、同步数据 共同组成一个Eureka集群
(你中有我 我中有你 互通有无)

1 全景

2 yml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
yaml复制代码# 给当前服务取一个名字
spring:
application:
name: eureka

# 设置端口号
server:
port: 1234


# 默认情况下 Eureka Server也是一个普通的微服务 即当它是一个注册中心的时候 他有2种 1:注册中心 2:普通服务(把自己注册到自己的上面去)
# 比喻不形象 Server是一个结婚登记处 1:他是一个登记员 登记其他人的结婚信息 2:同时他也是人类的一员 他也可能结婚 那么自己登记自己老 也不是不可以

# register-with-eureka为false 表示不把当前项目注册到服务中心上
# fetch-registry为false 表示不从Eureka Server上获取注册信息
eureka:
client:
register-with-eureka: true
fetch-registry: true
# A服务要注册到B上面
service-url:
defaultZone: http://eurekaB:1235/eureka
instance:
hostname: eurekaA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
yaml复制代码# 给当前服务取一个名字
spring:
application:
name: eureka

# 设置端口号
server:
port: 1235

# 默认情况下 Eureka Server也是一个普通的微服务 即当它是一个注册中心的时候 他有2种 1:注册中心 2:普通服务(把自己注册到自己的上面去)
# 比喻不形象 Server是一个结婚登记处 1:他是一个登记员 登记其他人的结婚信息 2:同时他也是人类的一员 他也可能结婚 那么自己登记自己老 也不是不可以

# register-with-eureka为false 表示不把当前项目注册到服务中心上
# fetch-registry为false 表示不从Eureka Server上获取注册信息
eureka:
client:
register-with-eureka: true
fetch-registry: true
# A服务要注册到B上面
service-url:
defaultZone: http://eurekaA:1234/eureka
instance:
hostname: eurekaB

3 打jar包

1
2
css复制代码java jar xxxx.jar --spring.profiles.active=a
java jar xxxx.jar --spring.profiles.active=b
1
复制代码启动2个实例时 可能会报一些错 不影响使用 页面登陆不了 排除你自己哪里写的有问题了

1
复制代码启动成功后 确实是相互注册了 共建了一个集群

5 Eureka细节

1
arduino复制代码Eureka分为Eureka Server和Eureka Client

1 Eureka Server

1
2
3
4
5
scss复制代码对外提供的功能:
(服务端的接收 发放 同步server端)
1.服务注册(接收) 所有服务都注册到server端
2.提供注册表(发放) 注册表将所有注册到服务上来的整理成一个列表 Eureka调用服务时 获取这个注册表 正常情况下 注册表会缓存下来 若缓存失效 直接获取最新的注册表
3.同步状态 Eureka Client通过注册、心跳等机制 和Eureka Server同步当前客户端的状态

2 Eureka Client

1
vbscript复制代码Eureka Client很多事情帮你做了(自动拉取 更新以及缓存server信息 哪怕所有server节点凉了 client依旧能获取到想要的地址(但是地址可能不太准确)) 简化每个服务和server之间的交互

2.1 服务注册

1
arduino复制代码服务提供者将自己注册到服务注册中心(server) 提示->服务提供者(业务划分上的划分) 本质上是Eureka Client 当Eureka Client向Eureka Server注册时 需要提供必要的元信息 比如IP地址 名称 运行状态 端口等。。。

2.2 服务续约

1
arduino复制代码Eureka Client注册到Eureka Server上之后 注册成功后 Eureka Client每隔30s向Eureka发送一条心跳信息(告诉它我还活着)  若连续90s(连续3次没法送)有没有收到Eureka Client的续约消息 它会认为Client挂了 赶紧把client从服务列表撤销
1
2
3
4
5
6
yaml复制代码eureka:
instance:
# 服务续约时间 默认为30s
lease-renewal-interval-in-seconds: 30
# 服务失效时间 默认为90s
lease-expiration-duration-in-seconds: 90

2.3 服务下线

1
vbscript复制代码当client下线时 发消息通知server我下线了

2.4 获取注册表信息

1
arduino复制代码Eureka Client从Eureka Server上获取服务的注册信息 将其缓存在本地 本地客户端调用服务器  会从该信息中查找远程服务所对应的IP地址等信息 Eureka Client上缓存服务定期30s更新 若本地缓存注册表的信息与server传过来的信息有变化的话 Client会自动处理
1
2
3
4
5
6
yaml复制代码eureka:
client
# 是否运行获取注册表信息
fetch-registry: true
# Client上缓存的服务注册信息 定期30s更新
registry-fetch-interval-seconds: 30

6 Eureka集群原理

1
2
3
4
5
6
7
rust复制代码在这个图上 Eureka通过Replicate进行数据同步 不同Eureka Server之间的节点地位均等 节点间集群的形成 通过置顶的serviceUrl相互注册 高可用节点的可用性得到提高 

若集群中某个节点失效 client自动切换到其他server上(每个server节点相互同步) Eureka连接方式 不建议单线连接(1->2->3 此时1也会和3互相同步) 可以指定多个注册地址(这样配置serviceUrl 1->2 1->3)

Eureka分区:
1.region->地理上的不同区域
2.zone->具体的机房

7 小结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vbscript复制代码1.注册中心的出现解决了服务间的耦合

2.Eureka三种角色 server(注册中心) client(provider(提供者) consumer(服务消费者) )

3.Eureka搭建 引入依赖 在application上开启server服务 在yml中配置服务名 端口号。。。启动成功后 会显示系统状态 副本 注册上来的服务信息 系统运行环境 以及该的一些ip等基本信息

4.eureka集群(类似互联网 离了你照转) 比如需要2个eureka yml配置 注 register-with-eureka: true fetch-registry: true 配置一个hostname serviceUrl->其他yml的(http://hostname:port/application name)
为了省事 打包运行 即可看到相互注册了

5.eureka细节 server -> 对外提供3个功能:给予服务注册(接收) 提供注册表 便于client的调用 同步 判断client是否活着 活着才能同步 否则就被杀掉(无情)
client -> 特色:自动拉取 更新 缓存信息
服务注册:服务提供者将自己注册上去(业务划分出2个角色 本质是一个人) 注册时需要提供一些元信息 ip啥的(类似身份证 手机号啥的 证明你是你开玩笑的 好认识你和你交朋友)
服务续约:注册上来 噩梦刚开始 client向server30s续约(费)一次 告诉server别杀我 我还活着 server连续3次90s未接收到client消息 掉线了(类似在黑吧 当你时间到了 给你3次机会 你还没有付费 你个穷鬼 别站在茅坑不拉屎 赶紧下来) 就这样被server从服务列表中剔除了
服务下线:告诉server我下线了(老板 我下线了 我结一下帐)
获取注册表信息(消费者):从server上获取信息 缓存本地 更新操作 -> 根据信息比对是否进行更新处理
提供者:向server注册 提供自身的信息 以及与server心跳时间(存活)的约定 以及服务下线的功能
消费者:从server获取服务注册信息 并缓存 定期对比该服务信息进行更新处理

6.集群
server每个节点地位平等 通过Replicate数据同步 serverUrl相当于拨号 节点相互之间的连接方式(某个节点崩溃怎么办) 脚踏"两"只船

本文转载自: 掘金

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

Elasticsearch7——动态映射与显式映射 一、什么

发表于 2021-11-25

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

这是Elasticsearch7。15:Mapping文档的译文,根据资料与个人理解梳理而成。


一、什么是映射(Mapping)

由 【Elasticsearch7——索引、type、文档与字段】 一节可知,Elasticsearch中是按文档的形式存储数据的,处理后通过索引可以进行查询。而映射(mapping)是原生JSON到文档的转换器;也决定了索引与文档如何进行关联。

如下所示:

分词.png

每个文档都是字段的集合,每个字段都有自己的 数据类型。映射数据时可以定义一个映射,包含与文档相关的字段列表。映射定义还包括元数据字段,例如_source字段定义了如何处理文档关联的元数据。

防止字段无限增加

如果使用了动态映射,插入的每个新文档都可能引入新字段。在索引中定义太多字段会导致映射爆炸,从而导致内存不足的错误和难以恢复的情况。使用映射限制设置来限制字段映射的数量(手动或动态创建)并防止映射爆炸。

  • index.mapping.total_fields.limit:限制了索引中的字段最大数量;字段、对象映射以及字段别名计入此限制,默认值为1000。限制的目的是为了防止映射和搜索变得太大。较高的值会导致性能下降和内存问题,尤其是在负载高或资源很少的集群中。
  • index.mapping.depth.limit:表示映射的深度。例如,如果所有字段都在根对象级别定义,则深度为1。如果有一个对象映射,则深度为 2等。默认为20。
  • index.mapping.nested_fields.limit:定义了nested索引中不同映射的最大数量,nested类型只应在需要相互独立地查询对象数组时使用;默认为50。
  • index.mapping.nested_objects.limit:单个文档可以包含的嵌套 JSON 对象(nested类型)的最大数量,默认为10000。
  • index.mapping.field_name_length.limit:设置字段名称的最大长度,默认为 Long.MAX_VALUE(无限制)。
  • index.mapping.dimension_fields.limit:仅供 Elastic 内部使用,索引的最大时间序列维度数;默认为16。

dynamic 参数

dynamic参数控制是否动态添加新字段,取值如下所示:

取值
true 新字段被添加到映射中(默认)
runtime 新字段作为运行时字段添加到映射中,这些字段未编入索引,并_source在查询时加载
false 新字段将被忽略,这些字段不会被索引或可搜索
strict 如果检测到新字段,则会抛出异常并拒绝文档,新字段必须显式添加到映射中

搜索分为两个阶段:查询时输入即搜索、索引时输入即搜索,这里暂时挖个坑[todo]。而运行时字段是在查询时计算的字段,可以:

  • 在不重新索引数据的情况下向现有文档添加字段
  • 在不了解数据结构的情况下开始处理数据
  • 在查询时覆盖从索引字段返回的值
  • 为特定用途定义字段而不修改底层架构

好处

  • 由于运行时字段未编入索引,因此添加运行时字段不会增加索引大小;
  • 如果将运行时字段设为索引字段,则无需修改任何引用运行时字段的查询;
  • 简化了映射决策,不必预先决定如何解析数据;
  • 使用异步搜索 API运行包含运行时字段的搜索。这种搜索方法有助于抵消为包含该字段的每个文档中的运行时字段计算值的性能影响。

使用运行时字段无需重新索引即可更改架构,可以结合使用运行时字段和索引字段来平衡资源使用和性能。这样可以使索引更小,搜索性能更慢。

二、动态映射

动态映射允许 Elasticsearch 动态地添加其他字段,只需索引文档即可。Elasticsearch 最重要的特性之一是约定,可以直接添加索引文档,无需创建索引、定义映射类型和定义字段。

新字段的自动检测和添加称为 动态映射,也可以自定义动态映射规则:

  • 动态字段映射:管理检测动态字段的规则
  • 动态模板:为动态添加的字段配置自定义映射规则;索引模板可以为新索引配置默认映射、设置和别名,无论是自动创建还是显式创建。

可以在文档和object级别禁用动态映射,将dynamic参数设置为 false忽略新字段,设置为strict会在 Elasticsearch 遇到未知字段时拒绝文档。

动态字段映射

当 Elasticsearch 在文档中检测到新字段时,它默认动态地将该字段添加到类型映射中;可以通过dynamic 参数控制此行为。

启用动态映射:

  • 当dynamic=true时,索引时输入即搜索
  • 当dynamic=runtime时,查询时输入即搜索

dynamic 参数的设置影响JSON数据解析,默认数据类型解析如下:

json数据 dynamic=true dynamic=runtime
null 不添加 不添加
true 或者 false boolean boolean
double float double
integer long long
object object 不添加字段
array 取决于数组中的第一个非null值 取决于数组中的第一个非null值
string【通过日期检测】 date date
string【通过数值检测 float 或者 long double 或者 long
string【未通过日期或数值检测】 text带有.keyword子字段 keyword

date_detection(日期检测)

如果date_detection是enabled(默认),检查新的字符串字段以查看其内容是否与 中指定的任何日期模式匹配 dynamic_date_formats。如果找到匹配项,date则会添加一个具有相应格式的新字段。

dynamic_date_formats的默认值为:

  • yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ
  • yyyy-MM-dd
  • yyyy/MM/dd HH:mm:ss Z
  • yyyy/MM/dd Z

禁用日期检测:

1
2
3
4
5
6
perl复制代码PUT my-index-000001
{
"mappings": {
"date_detection": false
}
}

自定义日期检测:

1
2
3
4
5
6
7
8
9
10
11
perl复制代码PUT my-index-000001
{
"mappings": {
"dynamic_date_formats": ["MM/dd/yyyy"]
}
}

PUT my-index-000001/_doc/1
{
"create_date": "09/25/2015"
}

数值检测(numeric_detection):

1
2
3
4
5
6
7
8
9
10
11
12
perl复制代码PUT my-index-000001
{
"mappings": {
"numeric_detection": true
}
}

PUT my-index-000001/_doc/1
{
"my_float": "1.0",
"my_integer": "1"
}

动态映射模板

除了默认动态字段映射规则之外,将dynamic设置为true或者runtime,然后就可以通过动态模板自定义映射规则了。动态模板在添加后可以重新排序或删除,匹配规则如下:

  • match_mapping_type:按类型验证,支持默认的数据类型
  • match/unmatch:按字段名称进行模式匹配
  • path_match/path_unmatch:按属性访问路径进行模式匹配
  • 未定义以上的匹配规则时,不匹配任何字段

验证映射

如果提供的映射包含无效的映射片段,则返回验证错误。在应用索引的动态模板时进行验证,并且在大多数情况下,在动态模板更新时进行验证。在某些情况下,提供无效的映射片段可能会导致动态模板的更新或验证失败:

  • 如果match_mapping_type没有指定,但是模板至少能匹配一个预定义的映射类型,则该映射片段被视为有效的。如果一个字段在匹配时可以被不同类型进行索引,那么在索引时会返回验证错误。所以,建议将match_mapping_type配置为预期的JSON类型。
  • 如果在映射片段中使用{name}占位符,在更新动态模板时跳过验证,因为名称未知。

match_mapping_type、match/unmatch、path_match/path_unmatch

  1. 动态模板设置运行时字段

动态模板可以设置运行时字段,如下所示。当匹配时,会将字段映射为类型为ip的运行时字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
perl复制代码PUT my-index-000001/
{
"mappings": {
"dynamic_templates": [
{
"strings_as_ip": {
"match_mapping_type": "string",
"match": "ip*",
"runtime": {
"type": "ip"
}
}
}
]
}
}
  1. match_mapping_type

match_mapping_type中的数据类型可以被JSON解析器检测。因为 JSON 不区分 long和 integer或 double和 float,所以它总是选择更广泛的数据类型,例如long整数和double浮点数。

例如,将long映射为integer

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
perl复制代码PUT my-index-000001
{
"mappings": {
"dynamic_templates": [
{
"integers": {
"match_mapping_type": "long",
"mapping": {
"type": "integer"
}
}
},
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"type": "text",
"fields": {
"raw": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
]
}
}
  1. match/unmatch

match参数使用模式匹配字段名称;而unmatch使用模式排除匹配的字段。

1
2
json复制代码  "match_pattern": "regex",
"match": "^profit_\d+$"
  1. path_match 和 path_unmatch

path_match与path_unmatch参数与match/unmatch类似,只是匹配的是路径而不是字段名称。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
perl复制代码PUT my-index-000001
{
"mappings": {
"dynamic_templates": [
{
"full_name": {
"path_match": "name.*",
"path_unmatch": "*.middle",
"mapping": {
"type": "text",
"copy_to": "full_name"
}
}
}
]
}
}

模板变量

{name}占位符在mapping 中会被替换为字段名称;{dynamic_type}占位符在mapping 中会被替换为动态类型。

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
perl复制代码PUT my-index-000001
{
"mappings": {
"dynamic_templates": [
{
"named_analyzers": {
"match_mapping_type": "string",
"match": "*",
"mapping": {
"type": "text",
"analyzer": "{name}"
}
}
},
{
"no_doc_values": {
"match_mapping_type":"*",
"mapping": {
"type": "{dynamic_type}",
"doc_values": false
}
}
}
]
}
}

查询映射

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
bash复制代码GET test/_mapping

Result:
{
"test" : {
"mappings" : {
"properties" : {
"description" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"title" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}

三、显式映射

显式映射:可以精确的约束映射的定义,如

  • 哪些字符串字段应被视为全文字段
  • 哪些字段包含数字、日期或地理位置
  • 日期值 的格式
  • 控制动态添加字段映射的自定义规则

创建索引时显式设置映射:

1
2
3
4
5
6
7
8
9
10
perl复制代码PUT /my-index-000003
{
"mappings": {
"properties": {
"age": { "type": "integer" },
"email": { "type": "keyword" },
"name": { "type": "text" }
}
}
}

更新映射(添加属性):

1
2
3
4
5
6
7
8
9
bash复制代码PUT /my-index-000003/_mapping
{
"properties": {
"employee-id": {
"type": "keyword",
"index": false
}
}
}

更改现有字段可能会使已编入索引的数据无效,如果需要更改现有字段的映射,请创建一个新的数据流并将您的数据重新索引到其中。


查看索引:

1
2
bash复制代码# 查看索引
GET /my-index-000003/_mapping

API 返回以下响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
json复制代码{
"my-index-000003" : {
"mappings" : {
"properties" : {
"age" : {
"type" : "integer"
},
"email" : {
"type" : "keyword"
},
"employee-id" : {
"type" : "keyword",
"index" : false
},
"name" : {
"type" : "text"
}
}
}
}
}

查看索引的特定字段:

1
2
bash复制代码# 查看索引的特定字段
GET /my-index-000003/_mapping/field/age

API 返回以下响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码{
"my-index-000003" : {
"mappings" : {
"age" : {
"full_name" : "age",
"mapping" : {
"age" : {
"type" : "integer"
}
}
}
}
}
}

四、参考文档

Mapping

本文转载自: 掘金

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

Java反射(二):反射的一些基本操作

发表于 2021-11-25

对于任何一个类class实例,我们通过反射(获取其Class类实例)就可以获取它的一切信息。

获取字段信息(Field类)

Class类提供了以下几个实例方法来获取Field类:

  • Field getField(String name):根据字段名获取某个public的field(包括父类)
  • Field getDeclaredField(String name):根据字段名获取当前类的某个field(不包括父类)
  • Field[] getFields():获取所有public的field(包括父类)
  • Field[] getDeclaredFields():获取当前类的所有field(不包括父类)

注:可以使用void setAccessible(boolean flag)来决定是否可以访问非public字段。如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。

一个Field类实例包含了一个字段的所有信息:

  • String getName():返回字段名称
  • Class<?> getType():返回字段类型信息(Class类实例,如String.class)
  • int getModifiers():返回字段的修饰符信息,Modifier类的静态方法isXXX(如Modifier.isPublic(int))根据其int返回值进行位运算来判断是否含有某个修饰符

获取/设置字段值(getter/setter)

不仅可以利用反射拿到类class字段的一个Field类实例,还可以利用Object get(Object obj)方法获取类class实例字段的具体值和void set(Object obj, Object value)方法设置字段的值。

注:参数obj为类class实例,若为静态字段,只需将实参obj赋为null

获取方法信息

我们不仅可以通过Class类实例获取Field类实例来得到字段信息,同样的,也可以获取Method类来得到方法信息。

Class类提供了以下实例方法来获取Method类:

  • Method getMethod(name, Class...):获取某个public的Method(包括父类)
  • Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
  • Method[] getMethods():获取所有public的Method(包括父类)
  • Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)

注:同样含有void setAccessible(boolean flag)方法

一个Method实例包含一个方法的所有信息:

  • String getName():返回方法名称
  • Class<?> getReturnType():返回方法返回值类型
  • Class<?>[] getParameterTypes():返回方法的参数类型
  • int getModifiers():返回方法的修饰符(同Field类)

调用方法

当我们获取到一个Method类实例时,可以通过Object invoke(Object obj, Object... args)调用。

注:对Method实例调用invoke就相当于调用该方法,invoke的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致。若为为静态方法,只需将obj赋为null

使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。

调用构造方法

我们一般使用new操作符创建新的实例。如果通过反射创建新的实例,可以调用Class类提供的newInstance()方法。

newInstance()方法的局限在于,它只能调用类class的public的无参构造方法。

为了调用任意的构造方法,Java的反射API提供了Constructor类,它包含一个构造方法的所有信息。通过Constructor实例可以创建一个实例对象:T new Instance(Object... parameters)。

Class类提供了以下实例方法来获取Construct类:

  • Constructor<T> getConstructor(Class...):获取某个public的Constructor
  • Constructor<T> getDeclaredConstructor(Class...):获取某个Constructor
  • Constructor<?>[] getConstructors():获取所有public的Constructor
  • Constructor<?>[] getDeclaredConstructors():获取所有Constructor

注:同样含有void setAccessible(boolean flag)方法

本文转载自: 掘金

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

Innodb为什么不使用朴素LRU缓存? 写在前面 什么是b

发表于 2021-11-25

写在前面

  1. 为什么Innodb buffer 需要用LRU缓存?
  2. 为什么不用朴素LRU策略淘汰内存页?
  3. 改进版的LRU缓存能解决什么问题,为什么能解决?

什么是buffer pool

我们知道,mysql(使用innodb存储引擎)的数据存在磁盘上,当需要查询,修改数据时,系统会将该数据所在的页加载到内存中一个叫buffer pool的区域再进行操作

  • 若是进行读取操作,使用完毕后不会立即将该页从内存中移除,而是将其缓存起来,这样当下次还需要使用就能避免一次磁盘IO
  • 若是对该页进行了修改,使其变成了脏页,也不会立即将脏页刷回磁盘,而是通过记录redo log和后台任务定时将脏页刷回磁盘,这样既保证了数据的持久性,也通过将多个随机IO合并成少数顺序IO提高IO性能

用LRU策略淘汰缓存页

buffer pool能使用的内存大小是有限的,我们可以通过以下命令来查看其大小

SHOW VARIABLES LIKE ‘innodb_buffer_pool_size’;

+———————–+———-+

|Variable_name |Value |

+———————–+———-+

|innodb_buffer_pool_size|4294967296|

+———————–+———-+

查询结果显示,这台msyql的buffer pool大小为4GB

随着程序的运行,若需要缓存的页面超过buffer pool大小,则需要将某些旧页面移除,这样才能装下即将要使用的页面。那移除哪些页面呢?为了使缓存命中率增大,减少磁盘IO次数,一种策略为:淘汰最近最少未使用的页面,这个页面在过去的一段时间最少被用到,根据时间局部性原理,预计在未来一段时间内也最少被用到,因此淘汰这个页面是合理的,这就是朴素LRU(Least recently used)策略

这样当访问某个页面时:

  • 若该页面已经在buffer pool,则将其从链表取出,加入LRU链表的头部
  • 若该页面不在buffer pool中,则从磁盘加载该页面,也加入LRU链接的头部,当空间不够时,淘汰掉链表尾部的页(若为脏页需刷回磁盘)

这样当buffer pool中某个页面一直未被使用时,就会逐渐移动到链接尾部,最终被淘汰

image.png

朴素LRU策略的问题

以上缓存淘汰策略在正常情况下能很好工作,但若遇到以下两种情况会导致buffer pool淘汰掉不是“最近最少使用”的数据,和LRU策略的初衷南辕北辙,进而降低缓存命中率,影响整个程序性能

  • 预读:根据空间局部性原理,mysql若认为可能使用当前页面周围的页面,就会将这些周围的页面异步加载到buffer pool,具体分为线性预读和随机预读
+ **线性预读**:若顺序读取某个区(一个区有64个页)中的页面数量超越一定的数量,则预先将下一个区的全部页面读入到内存,该数量由系统变量`innodb_read_ahead_threshold`控制,默认为56
  • 随机预读:当buffer pool已经有一个区中的一些页面时,会预先将该区中的其他页面也写入buffer pool。是否开启这个功能由系统变量innodb_random_read_ahead控制,默认不开启

若预读出来的页面很快被使用,则该功能能降低响应时间,提升程序性能。但如果后续没有使用,且由于新加入这些页面,而淘汰掉一些后面会用到的老页面,则会起到反效果

  • 全表扫描:即将某个表的所有页都加载到buffer pool,若该表页面较多,则会把原本buffer pool中的页面都淘汰掉。这样程序想用以前的页面时,需要从磁盘加载一次

一般来说全表扫描触发的频率很低,于是上面的做法等于是将短时间内不会再用的页面缓存起来,而将未来大概率用到的页面驱逐出内存,和高效率的做法完全相反,极大的影响效率

改进版LRU

于是mysql将该LRU链接划分为两部分:

  • 前面一部分存储访问频率较高的热数据,称为young区域
  • 后面一部分存储访问频率较低的冷数据,称为old区域

image.png
每一部分占多大比例由系统变量innodb_old_blocks_pct控制,默认为37,即冷数据区域占37%

+———————+—–+

|Variable_name |Value|

+———————+—–+

|innodb_old_blocks_pct|37 |

+———————+—–+

当访问某个页面时,若该页面不在内存,即第一次访问,则将其放到old区域头部

这样若预读出来的页面后续没有使用,则会逐步被淘汰,且不会影响到频繁访问的老页面

到现在为止,young区域还没有用到,那什么时候可以将old区域的页晋升到young区域呢?即满足什么条件下,系统认为某个页面是属于热页面

  • 我们先看第一种策略:第二次访问该页则晋升
+ 除了加载页面的访问以外,若后续再访问该页面,就将其晋升到young区域。该策略完美适用于预读场景,但不适用于全表扫描的场景,你想,全表扫描将页面加载到`buffer pool`后,紧接着马上访问该页面的所有记录,每访问一条记录,相当于访问一次该页。若每条记录较小,算100字节,一个16k的页面大概能装160条记录,总共需要被访问160次。也就是说不仅是第二次访问,`可能短时间内对一个页面访问了很多次,但该页面在业务上来说并不属于热数据`
  • 那咋办?mysql采用了第二种策略:访问间隔时长晋升
+ 全表扫描有个特点,对每个页的访问**集中在很短的时间内**,因此该策略为:

第一次访问该页面时记录一个初始访问时间,若后续对该页面的访问时的时间,减去初始访问时间大于某个值,则将该页面加入LRU链表的热数据区

这里的“某个值”,即时间间隔的阈值由系统变量innodb_old_blocks_time控制,默认为1s(通常在内存访问一个页面的所有记录要不了1s)

+———————-+—–+

|Variable_name |Value|

+———————-+—–+

|innodb_old_blocks_time|1000 |

+———————-+—–+

也就是说,从第一次从磁盘加载某个页面后的1秒后,还有请求会访问该页,不论该页是不是因为全表扫描而进入buffer pool,该页都是一个会被高频访问的页,是热数据。而由于全表扫描而进入buffer pool中的页大部分都不会再被访问,是冷数据,也就会慢慢被淘汰出内存

该策略对于全表扫描的场景,也能有效防止淘汰掉真正频繁使用的页

总结

  • 因为innodb buffer pool空间有限,但没有可用空间时,需要选择一部分页面淘汰掉,以便装入即将使用的页面
  • 朴素LRU缓存淘汰策略在大多数场景下能淘汰掉最近最少使用的页面,提高缓存命中率,但不适用于预读和全表扫描的场景
  • 通过将LRU链表划分为冷热两部分,第一次访问时将页放入冷数据区域,一定时间间隔后还有对该页的访问,则将该页放入热数据区域,使得在这两种场景下也能保证真正的热页面常驻内存,提高了缓存命中率

本文转载自: 掘金

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

LeetCode 79 单词搜索【c++/java详细题解

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」。 来源于湖科大教书匠

1、题目

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例 1:

1
2
arduino复制代码输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true

示例 2:

1
2
arduino复制代码输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true

示例 3:

1
2
arduino复制代码输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
输出:false

提示:

  • m == board.length
  • n = board[i].length
  • 1 <= m, n <= 6
  • 1 <= word.length <= 15
  • board 和 word 仅由大小写英文字母组成

2、思路

(回溯) O(n23k)O(n^2 3^k)O(n23k)

深度优先搜索,我们定义这样一种搜索顺序,即先枚举单词的起点,然后依次枚举单词的每个字母。在这个过程中需要将已经使用过的字母改成一个特殊字母,以避免重复使用字符。


递归函数设计:

1
c复制代码bool dfs(vector<vector<char>>& board, string& word,int u,int x,int y)

u代表当前枚举到了目标单词word第u个位置。

x,y是当前搜索到的二维字符网格的横纵坐标。

搜索过程如下:

  • 1、在二维字符网格中枚举每个单词的起点。
  • 2、从该起点出发向四周搜索单词word,并记录此时枚举到单词word的第u个位置 ( u从0开始)。
  • 3、如果当前搜索的位置(x,y)的元素board[x][y] == word[u],则继续向四周搜索。
  • 4、直到枚举到单词word的最后一个字母返回ture,否则返回false。

递归边界:

  • 1、当搜索过程出现当前位置board[x][y] != word[u] ,说明当前路径不合法,返回false。
  • 2、u == word.size() - 1,成功搜索到单词末尾,返回true。

实现细节:

  • 1、搜索过的位置继续搜索下一层时,需要对当前位置进行标识,表示已经搜索
  • 2、可以使用偏移数组来简化代码。


时间复杂度分析: 单词起点一共有 n2n^2n2 个,单词的每个字母一共有上下左右四个方向可以选择,但由于不能走回头路,所以除了单词首字母外,仅有三种选择。所以总时间复杂度是 O(n23k)O(n^2 3^k)O(n23k) 。

3、c++代码

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
c复制代码class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
for(int i = 0; i < board.size(); i++)
for(int j = 0; j < board[i].size(); j++)
if(dfs(board,word,0,i,j)) return true;
return false;
}
int dx[4] = {-1,0,1,0}, dy[4] = {0,1,0,-1}; //方向数组
bool dfs(vector<vector<char>>& board, string& word,int u,int x,int y)
{
if(board[x][y] != word[u]) return false;
if(u == word.size() - 1) return true;
char t = board[x][y];
board[x][y] = '.';
for(int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
//出界或者走到已经搜索过的位置
if(a < 0 || a >= board.size() || b < 0 || b >= board[0].size() || board[a][b] == '.') continue;
if(dfs(board,word,u+1,a,b)) return true;
}
board[x][y] = t;
return false;
}
};

4、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
java复制代码class Solution {
public boolean exist(char[][] board, String word) {
for(int i = 0; i < board.length; i++)
for(int j = 0; j < board[i].length; j++)
if(dfs(board,word,0,i,j)) return true;
return false;
}
int[] dx = new int[]{-1,0,1,0}, dy = new int[]{0,1,0,-1};
boolean dfs(char[][] board, String word,int u,int x,int y)
{
if(board[x][y] != word.charAt(u)) return false;
if(u == word.length() - 1) return true;
char t = board[x][y];
board[x][y] = '.';
for(int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if(a < 0 || a >= board.length|| b < 0 || b >= board[0].length || board[a][b] == '.') continue;
if(dfs(board,word,u+1,a,b)) return true;
}
board[x][y] = t;
return false;
}
}

原题链接:79. 单词搜索
在这里插入图片描述

本文转载自: 掘金

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

单例模式

发表于 2021-11-25

一、单例模式介绍

image.png

单例模式可以说是整个设计中最简单的模式之一,而且这种方式即使在没有看设计模式相关资料也会常用在编码开发中。

因为在编程开发中经常会遇到这样一种场景,那就是需要保证一个类只有一个实例哪怕多线程同时访问,并需要提供一个全局访问此实例的点。

综上以及我们平常的开发中,可以总结一条经验,单例模式主要解决的是,一个全局使用的类频繁的创建和消费,从而提升提升整体的代码的性能。

二、案例场景

本章节的技术所出现的场景非常简单也是我们日常开发所能见到的,例如;

  1. 数据库的连接池不会反复创建
  2. spring中一个单例模式bean的生成和使用
  3. 在我们平常的代码中需要设置全局的的一些属性保存

在我们的日常开发中大致上会出现如上这些场景中使用到单例模式,虽然单例模式并不复杂但是使用面却比较广。

三、7种单例模式实现

单例模式的实现方式比较多,主要在实现上是否支持懒汉模式、是否线程安全中运用各项技巧。当然也有一些场景不需要考虑懒加载也就是懒汉模式的情况,会直接使用static静态类或属性和方法的方式进行处理,供外部调用。

那么接下来我们就通过实现不同方式的实现进行讲解单例模式。

0. 静态类使用

1
2
3
4
5
typescript复制代码public class Singleton_00 {

public static Map<String,String> cache = new ConcurrentHashMap<String, String>();

}
  • 以上这种方式在我们平常的业务开发中非常场常见,这样静态类的方式可以在第一次运行的时候直接初始化Map类,同时这里我们也不需要到延迟加载在使用。
  • 在不需要维持任何状态下,仅仅用于全局访问,这个使用使用静态类的方式更加方便。
  • 但如果需要被继承以及需要维持一些特定状态的情况下,就适合使用单例模式。

1. 懒汉模式(线程不安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码public class Singleton_01 {

private static Singleton_01 instance;

private Singleton_01() {
}

public static Singleton_01 getInstance(){
if (null != instance) return instance;
instance = new Singleton_01();
return instance;
}

}
  • 单例模式有一个特点就是不允许外部直接创建,也就是new Singleton_01(),因此这里在默认的构造函数上添加了私有属性 private。
  • 目前此种方式的单例确实满足了懒加载,但是如果有多个访问者同时去获取对象实例你可以想象成一堆人在抢厕所,就会造成多个同样的实例并存,从而没有达到单例的要求。

2. 懒汉模式(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码public class Singleton_02 {

private static Singleton_02 instance;

private Singleton_02() {
}

public static synchronized Singleton_02 getInstance(){
if (null != instance) return instance;
instance = new Singleton_02();
return instance;
}

}
  • 此种模式虽然是安全的,但由于把锁加到方法上后,所有的访问都因需要锁占用导致资源的浪费。如果不是特殊情况下,不建议此种方式实现单例模式。

3. 饿汉模式(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码public class Singleton_03 {

private static Singleton_03 instance = new Singleton_03();

private Singleton_03() {
}

public static Singleton_03 getInstance() {
return instance;
}

}
  • 此种方式与我们开头的第一个实例化Map基本一致,在程序启动的时候直接运行加载,后续有外部需要使用的时候获取即可。
  • 但此种方式并不是懒加载,也就是说无论你程序中是否用到这样的类都会在程序启动之初进行创建。
  • 那么这种方式导致的问题就像你下载个游戏软件,可能你游戏地图还没有打开呢,但是程序已经将这些地图全部实例化。到你手机上最明显体验就一开游戏内存满了,手机卡了,需要换了。

4. 使用类的内部类(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码public class Singleton_04 {

private static class SingletonHolder {
private static Singleton_04 instance = new Singleton_04();
}

private Singleton_04() {
}

public static Singleton_04 getInstance() {
return SingletonHolder.instance;
}

}
  • 使用类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的方式耗费性能。
  • 这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确的加载。
  • 此种方式也是非常推荐使用的一种单例模式

5. 双重锁校验(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码public class Singleton_05 {

private static volatile Singleton_05 instance;

private Singleton_05() {
}

public static Singleton_05 getInstance(){
if(null != instance) return instance;
synchronized (Singleton_05.class){
if (null == instance){
instance = new Singleton_05();
}
}
return instance;
}

}
  • 双重锁的方式是方法级锁的优化,减少了部分获取实例的耗时。
  • 同时这种方式也满足了懒加载。

6. CAS「AtomicReference」(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
csharp复制代码public class Singleton_06 {

private static final AtomicReference<Singleton_06> INSTANCE = new AtomicReference<Singleton_06>();

private static Singleton_06 instance;

private Singleton_06() {
}

public static final Singleton_06 getInstance() {
for (; ; ) {
Singleton_06 instance = INSTANCE.get();
if (null != instance) return instance;
INSTANCE.compareAndSet(null, new Singleton_06());
return INSTANCE.get();
}
}

public static void main(String[] args) {
System.out.println(Singleton_06.getInstance()); // org.itstack.demo.design.Singleton_06@2b193f2d
System.out.println(Singleton_06.getInstance()); // org.itstack.demo.design.Singleton_06@2b193f2d
}

}
  • java并发库提供了很多原子类来支持并发访问的数据安全性;AtomicInteger、AtomicBoolean、AtomicLong、AtomicReference。
  • AtomicReference<V> 可以封装引用一个V实例,支持并发访问如上的单例方式就是使用了这样的一个特点。
  • 使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。
  • 当然CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。

7. Effective Java作者推荐的枚举单例(线程安全)

1
2
3
4
5
6
7
8
csharp复制代码public enum Singleton_07 {

INSTANCE;
public void test(){
System.out.println("hi~");
}

}

约书亚·布洛克(英语:Joshua J. Bloch,1961年8月28日-),美国著名程序员。他为Java平台设计并实作了许多的功能,曾担任Google的首席Java架构师(Chief Java Architect)。

  • Effective Java 作者推荐使用枚举的方式解决单例模式,此种方式可能是平时最少用到的。
  • 这种方式解决了最主要的;线程安全、自由串行化、单一实例。
  • 调用方式*
1
2
3
typescript复制代码@Test
public void test() {
Singleton_07.INSTANCE.test();

这种写法在功能上与共有域方法相近,但是它更简洁,无偿地提供了串行化机制,绝对防止对此实例化,即使是在面对复杂的串行化或者反射攻击的时候。虽然这中方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

但也要知道此种方式在存在继承场景下是不可用的。

四、总结

  • 虽然只是一个很平常的单例模式,但在各种的实现上真的可以看到java的基本功的体现,这里包括了;懒汉、饿汉、线程是否安全、静态类、内部类、加锁、串行化等等。
  • 在平时的开发中如果可以确保此类是全局可用不需要做懒加载,那么直接创建并给外部调用即可。但如果是很多的类,有些需要在用户触发一定的条件后(游戏关卡)才显示,那么一定要用懒加载。线程的安全上可以按需选择。
  • 建议在学习的过程中一定要加以实践,否则很难完完整整的掌握一整套的知识体系。例如案例中的出现的Effective Java一书也非常建议大家阅读。另外推荐下这位大神的Github:github.com/jbloch

原文:重学 Java 设计模式:实战单例模式「7种单例模式案例,Effective Java 作者推荐枚举单例模式」

注:在此推荐小傅哥的重学java设计模式纸质书,文章摘抄自电子书仅限个人学习使用。

本文转载自: 掘金

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

程序环境和预处理 真正的认识C语言 程序环境和预处理

发表于 2021-11-25

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

程序环境和预处理

程序的翻译环境和执行环境

在ANSI C的任何一种实现中,存在两个不同的环境

第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令

第二种是执行环境,他用于实际执行代码

image-20210929062446170

详解编译+链接

编译环境

image-20210929063247796

组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

image-20210929063311384

image-20210929081838170

预处理详解

预定义符号

1
2
3
4
5
c复制代码__FILE__    //进行编译的源文件
__LINE__   //文件当前的行号
__DATE__   //文件被编译的日期
__TIME__   //文件被编译的时间
__STDC__   //如果编译器遵循ANSI C,其值为1,否则未定义

这些预定义符号都是语言内置的

image-20211009050748724

image-20211009051336290

但是gcc是支持的 gcc对c语言的支持非常好

#define

在c语言中#define是可以做两件事的

#define 定义符号

语法:#define name stuff

image-20211009052438743

1
2
3
4
5
6
7
8
9
c复制代码#define MAX 1000
#define reg register      //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)   //用更形象的符号来替换一种实现
#define CASE break;case     //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
             date:%s\ttime:%s\n" ,\
             __FILE__,__LINE__ ,    \
             __DATE__,__TIME__ )

image-20211009053424387

注意

在define定义标识符的时候,要不要在最后加上;

1
2
c复制代码#define MAX 1000;
#define MAX 1000

image-20211009055130245

#define 定义宏

image-20211009063939832

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

image-20211009061225500

我宏不加括号会咋样

image-20211009062353419

是不是觉的可以了,好我们就用你那个括号看看少了什么

image-20211009063624841

这恰恰也就证明了预处理(编译)只是进行文本操作

注意

参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

image-20211009064654448

define和typedef的区别

一个是替换另一个是重定义

image-20211009071624043

有一个题目

image-20211009072823622

#define替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意

  1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

image-20211009081444979

#和##

如何把参数插入到字符串中?

使用#,把一个宏参数变成对应的字符串。

image-20211009103630360

使用##

##于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。

image-20211009104721452

带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。

1
2
c复制代码x+1;//不带副作用
x++;//带有副作用

image-20211009110209148

image-20211009113746199

宏和函数对比

宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。

1
c复制代码#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不用函数来完成这个任务?

原因有二:
  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
当然和函数相比宏也有劣势的地方:
  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  2. 宏是没法调试的。(因为我们调试是在运行阶段调试的,宏是在编译阶段就替换了)
  3. 宏由于类型无关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

image-20211009145318151

宏和函数的一个对比

属性 #define定义宏 函数
代码长度 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度 更快 存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。
带有副作用的参数 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 函数参数只在传参的时候求值一次,结果更容易控制。
参数类型 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同。
调试 宏是不方便调试的 函数是可以逐语句调试的
递归 宏是不能递归的 函数是可以递归的

命名约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写

#undef

这条指令用于移除一个宏定义

image-20211009155513895

命令行定义(VS不支持,linux这个电脑没有哈哈下面就是概念自己看)

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一
个机器内存大写,我们需要一个数组能够大写。)

条件编译

满足条件代码就参与编译,不满足条件,代码就不参与编译

比如说:

调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

image-20211009161934319

常见的条件编译指令

#if 常量表达式

1
2
3
c复制代码#if 常量表达式
//...
#endif

image-20211009163553953

注意

必须是常量表达式,变量是不行的

image-20211009164717300

自己想想为什么只能是常量:这些变量是在运行的时候才会创建,但是#if 是在什么时候处理的呢,是在预编译阶段就处理好了,在创建这个局部变量的前面就处理好了

常量表达式由预处理器求值。

1
2
3
4
5
C复制代码//常量表达式由预处理器求值。
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif

image-20211009164024619

多个分支的条件编译

1
2
3
4
5
6
7
c复制代码#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

image-20211009165834094

判断是否被定义

1
2
3
4
c复制代码#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol

image-20211009171023343

嵌套指令

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif

文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。

头文件被包含的方式

本地文件包含

1
C复制代码#include "filename"

查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。

库文件包含

1
c复制代码#include <filename.h>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

嵌套文件包含

见我单片机那一偏博客应该详细一点

本文转载自: 掘金

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

【Python】 函数、高阶函数、匿名函数 与 函数属性 一

发表于 2021-11-25

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

一等对象与一等函数

在 Python 中,整数、字符串和字典都是一等对象。函数也是一等对象。人们经常将“把函数视作一等对象”简称为“一等函数”。

编程语言理论家把“一等对象”定义为满足下述条件的程序实体:

  • 在运行时创建
  • 能赋值给变量或数据结构中的元素
  • 能作为参数传给函数
  • 能作为函数的返回结果

高阶函数

接受函数为参数,或者把函数作为结果返回的函数是高阶函数。如 map 和 sorted 。

在函数式编程范式中,最为人熟知的高阶函数有 map 、filter 、reduce 和 apply 。

在 Python 3 中,map 和 filter 返回生成器(一种迭代器)。列表推导或生成器表达式具有 map 和 filter 两个函数的功能,而且更易于阅读。举个栗子:

1
2
3
4
5
python复制代码list(map(fact, range(6)))		# [1, 1, 2, 6, 24, 120] 阶乘
[fact(n) for n in range(6)] # [1, 1, 2, 6, 24, 120]

list(map(factorial, filter(lambda n: n % 2, range(6)))) # [1, 6, 120]
[factorial(n) for n in range(6) if n % 2] # [1, 6, 120]

在 Python 2 中,reduce 是内置函数,但是在 Python 3 中放到 functools 模块里了。

apply 函数在 Python 2.3 中标记为过时,在 Python 3 中移除了,因为不再需要它了。如果想使用不定量的参数调用函数,可以编写 fn(*args, **keywords) ,不用再编写 apply(fn, args, kwargs) 。

匿名函数

lambda 关键字在 Python 表达式内创建匿名函数。lambda 句法只是语法糖:与 def 语句一样,lambda 表达式会创建函数对象。

函数的属性

使用 dir 函数可以查看函数对象属性:dir(function_name)

类的实例没有而函数有的属性:

名称 类型 说明
__annotations__ dict 参数和返回值的注解
__call__ method-wrapper 实现 () 运算符;即可调用对象协议
__closure__ tuple 函数闭包,即自由变量的绑定(通常是 None )
__code__ code 编译成字节码的函数元数据和函数定义体
__defaults__ tuple 形式参数的默认值
__get__ method-wrapper 实现只读描述符协议
__globals__ dict 函数所在模块中的全局变量
__kwdefaults__ dict 仅限关键字形式参数的默认值
__name__ str 函数名称
__qualname__ str 函数的限定名称,如 Random.choice

本文转载自: 掘金

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

Mybatis 笔记——XML 配置

发表于 2021-11-25

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<!--配置-->
<configuration>
<!--属性-->
<properties>
<!--设置-->
<settings>
<!--类型别名-->
<typeAliases>
<!--类型处理器-->
<typeHandlers>
<!--对象工厂-->
<objectFactory>
<!--插件-->
<plugins>
<!--环境配置-->
<environments>
<!--数据库厂商标识-->
<databaseIdProvider>
<!--映射器-->
<mappers>
</configuration>
<!-- 注意元素节点的顺序!顺序不对会报错 -->

Properties

属性可以在外部文件进行配置,通过 <properties resource="jdbc.properties"/>引入外部文件

1
2
3
4
properties复制代码url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8
driver=com.mysql.jdbc.Driver
username=root
password=123456
1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<properties resource="jdbc.properties"/>

<!--环境配置-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments

也可以在 properties 元素的子元素中设置,并可以进行动态替换

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<!--环境配置-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>

Settings

常用的设置

设置名 描述 有效值 默认值
cacheEnabled 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 true false
lazyLoadingEnabled 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 true false
logImpl 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 SLF4J LOG4J

一个配置完整的 settings 元素的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
<setting name="defaultFetchSize" value="100"/>
<setting name="safeRowBoundsEnabled" value="false"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

TypeAliases

类型别名是为 Java 类型设置一个短的名字。它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余。

  1. 扫描类使用别名
1
2
3
xml复制代码<typeAliases>
<typeAlias type="pojo.User" alias="User"/>
</typeAliases>

当这样配置时,User可以用在任何使用pojo.User的地方。
2. 扫描包

1
2
3
xml复制代码<typeAliases>
<package name="pojo"/>
</typeAliases>

每一个在包 pojo 中的 JavaBean,在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。若有注解,则别名为其注解值。
3. 在类上使用注解

1
2
3
4
5
6
7
8
9
10
java复制代码import org.apache.ibatis.type.Alias;

@Alias("user")
public class User {

private int id;
private String name;
private String password;

}

下面是一些为常见的 Java 类型内建的类型别名。它们都是不区分大小写的,注意,为了应对原始类型的命名重复,采取了特殊的命名风格。

别名 映射的类型
_byte byte
_long long
_short short
_int int
_integer int
_double double
_float float
_boolean boolean
string String
byte Byte
long Long
short Short
int Integer
integer Integer
double Double
float Float
boolean Boolean
date Date
decimal BigDecimal
bigdecimal BigDecimal
object Object
map Map
hashmap HashMap
list List
arraylist ArrayList
collection Collection
iterator Iterator

Environments

配置 MyBatis 的多套运行环境,将 SQL 映射到多个不同的数据库上,必须指定其中一个为默认运行环境(通过 default 指定),通过 id 进行区别

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
xml复制代码<!--环境配置-->
<environments default="development">

<!--development环境-->
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>

<!--test-->
<environment id="test">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>

子元素节点:environment

  1. transactionManager 事务管理器
1
2
xml复制代码<!-- 语法 -->
<transactionManager type="[ JDBC | MANAGED ]"/>
  1. dataSource 数据源

使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。数据源是必须配置的。有三种内建的数据源类型:

1
xml复制代码type="[UNPOOLED|POOLED|JNDI]")
* unpooled:这个数据源的实现只是每次被请求时打开和关闭连接。
* **pooled**:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来 , 这是一种使得并发 Web 应用快速响应请求的流行处理方式。
* jndi:这个数据源的实现是为了能在如 Spring 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。

Mapper

Mapper 映射器告诉 MyBatis 到哪里去找映射文件

  1. 使用相对于类路径的资源引用
1
2
3
4
5
xml复制代码<mappers>

<mapper resource="mapper/UserMapper.xml"/>

</mappers>
  1. 类名
1
2
3
4
5
6
xml复制代码<!--需要配置文件名称和接口名称一致,并且位于同一目录下-->
<mappers>

<mapper class="mapper.UserMapper"/>

</mappers>
  1. 包名
1
2
3
4
5
6
xml复制代码<!--需要配置文件名称和接口名称一致,并且位于同一目录下-->
<mappers>

<package name="mapper"/>

</mappers>

Mapper 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="mapper.UserMapper">

<insert id="addUser" parameterType="pojo.User">
insert into user(id, name, password)
values (#{id}, #{name}, #{password})
</insert>

</mapper>
  • namespace 的命名必须跟某个接口同名
  • 接口中的方法与映射文件中sql语句id应该一一对应
  • namespace命名规则 : 包名+类名

本文转载自: 掘金

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

在 C/C++ 中反转字符串的不同方法

发表于 2021-11-25

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

给定一个字符串,编写一个 C/C++ 程序来反转它。

通过交换字符编写自己的反向函数: 一个简单的解决方案是编写我们自己的反向函数来反转C++ 中的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c++复制代码// 一个简单的 C++ 程序来反转字符串
#include <bits/stdc++.h>
using namespace std;

// 反转字符串的函数
void reverseStr(string& str)
{
int n = str.length();

// 从两个角开始交换字符
for (int i = 0; i < n / 2; i++)
swap(str[i], str[n - i - 1]);
}

// 驱动程序
int main()
{
string str = "haiyong";
reverseStr(str);
cout << str;
return 0;
}
  1. 输出 :
1
c++复制代码gnoyiah
  1. 使用内置的“反向”功能: “算法”头文件中有一个直接的功能可以进行反向操作,可以节省我们在编程时的时间。
1
2
3
c++复制代码// 反转 [begin, end] 中的元素
void reverse (BidirectionalIterator begin,
BidirectionalIterator end);
1
2
3
4
5
6
7
8
9
10
11
12
13
c++复制代码//一个快速编写的程序,用于使用 reverse() 反转字符串
#include <bits/stdc++.h>
using namespace std;
int main()
{
string str = "haiyong";

// 反向 str[begin..end]
reverse(str.begin(), str.end());

cout << str;
return 0;
}
  1. 输出 :
1
c++复制代码gnoyiah
  1. 只打印反向:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
c++复制代码// C++程序打印一个字符串的反转
#include <bits/stdc++.h>
using namespace std;

// 反转字符串的函数
void reverse(string str)
{
for (int i=str.length()-1; i>=0; i--)
cout << str[i];
}

// 驱动程序代码
int main(void)
{
string s = "haiyong";
reverse(s);
return (0);
}
  1. 输出:
1
c++复制代码gnoyiah
  1. 获取 const 字符串的反转:
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
c++复制代码// 获取const字符串反转的C++程序
#include <bits/stdc++.h>
using namespace std;

// 函数反转字符串并返回该字符串的反向字符串指针
char* reverseConstString(char const* str)
{
// 查找字符串的长度
int n = strlen(str);

// 创建动态指针字符数组
char *rev = new char[n+1];

// 将字符串复制到 ptr 数组
strcpy(rev, str);

// 从两个角开始交换字符
for (int i=0, j=n-1; i<j; i++,j--)
swap(rev[i], rev[j]);

// 反转字符串的返回指针
return rev;
}

// 驱动程序代码
int main(void)
{
const char *s = "haiyong";
printf("%s", reverseConstString(s));
return (0);
}
  1. 输出:
1
c++复制代码gnoyiah
  1. 使用构造函数反向字符串:将反向迭代器传递给构造函数返回一个反向字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
c++复制代码// 使用构造函数反转字符串的简单 C++ 程序
#include <bits/stdc++.h>
using namespace std;
int main(){

string str = "haiyong";

//反向迭代器的使用
string rev = string(str.rbegin(),str.rend());

cout<<rev<<endl;
return 0;
}
  1. 输出:
1
c++复制代码gnoyiah

5.使用临时字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c++复制代码// 使用构造函数反转字符串的简单 C++ 程序
#include <bits/stdc++.h>
using namespace std;
int main(){

string str = "haiyong";
int n=str.length();
//存储反向的临时字符串
string rev;
for(int i=n-1;i>=0;i--)
rev.push_back(str[i]);

cout<<rev<<endl;
return 0;
}

输出:

1
c++复制代码gnoyiah

本文转载自: 掘金

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

1…195196197…956

开发者博客

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