Dunwu Blog

大道至简,知易行难

Spring 访问 Elasticsearch

简介

Elasticsearch 是一个开源的、分布式的搜索和分析引擎。

通过 REST 客户端连接 Elasticsearch

如果在 classpath 路径下存在 org.elasticsearch.client:elasticsearch-rest-client jar 包,Spring Boot 会自动配置并注册一个 RestClient Bean,它的默认访问路径为:localhost:9200

你可以使用如下方式进行定制:

1
2
3
spring.elasticsearch.rest.uris=http://search.example.com:9200
spring.elasticsearch.rest.username=user
spring.elasticsearch.rest.password=secret

您还可以注册实现任意数量的 RestClientBuilderCustomizer bean,以进行更高级的定制。要完全控制注册,请定义 RestClient bean。

如果 classpath 路径有 org.elasticsearch.client:elasticsearch-rest-high-level-client jar 包,Spring Boot 将自动配置一个 RestHighLevelClient,它包装任何现有的 RestClient bean,重用其 HTTP 配置。

通过 Jest 连接 Elasticsearch

如果 classpath 上有 Jest,你可以注入一个自动配置的 JestClient,默认情况下是 localhost:9200。您可以进一步调整客户端的配置方式,如以下示例所示:

1
2
3
4
spring.elasticsearch.jest.uris=http://search.example.com:9200
spring.elasticsearch.jest.read-timeout=10000
spring.elasticsearch.jest.username=user
spring.elasticsearch.jest.password=secret

您还可以注册实现任意数量的 HttpClientConfigBuilderCustomizer bean,以进行更高级的定制。以下示例调整为其他 HTTP 设置:

1
2
3
4
5
6
7
8
static class HttpSettingsCustomizer implements HttpClientConfigBuilderCustomizer {

@Override
public void customize(HttpClientConfig.Builder builder) {
builder.maxTotalConnection(100).defaultMaxTotalConnectionPerRoute(5);
}

}

要完全控制注册,请定义 JestClient bean。

通过 Spring Data 访问 Elasticsearch

要连接到 Elasticsearch,您必须提供一个或多个集群节点的地址。可以通过将 spring.data.elasticsearch.cluster-nodes 属性设置为以逗号分隔的 host:port 列表来指定地址。使用此配置,可以像任何其他 Spring bean 一样注入 ElasticsearchTemplateTransportClient,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring.data.elasticsearch.cluster-nodes=localhost:9300
@Component
public class MyBean {

private final ElasticsearchTemplate template;

public MyBean(ElasticsearchTemplate template) {
this.template = template;
}

// ...

}

如果你添加了自定义的 ElasticsearchTemplateTransportClient @Bean ,就会替换默认的配置。

Elasticsearch Repositories

Spring Data 包含对 Elasticsearch 的 repository 支持。基本原则是根据方法名称自动为您构建查询。

事实上,Spring Data JPA 和 Spring Data Elasticsearch 共享相同的通用基础架构。

源码

完整示例:源码

使用方法:

1
2
3
mvn clean package
cd target
java -jar spring-boot-data-elasticsearch.jar

版本

Spring 和 Elasticsearch 匹配版本:

Spring Data Elasticsearch Elasticsearch Spring Framework Spring Boot
5.0.x 8.5.3 6.0.x 3.0.x
4.4.x 7.17.3 5.3.x 2.7.x
4.3.x 7.15.2 5.3.x 2.6.x
4.2.x[1] 7.12.0 5.3.x 2.5.x
4.1.x[1] 7.9.3 5.3.2 2.4.x
4.0.x[1] 7.6.2 5.2.12 2.3.x
3.2.x[1] 6.8.12 5.2.12 2.2.x
3.1.x[1] 6.2.2 5.1.19 2.1.x
3.0.x[1] 5.5.0 5.0.13 2.0.x
2.1.x[1] 2.4.0 4.3.25 1.5.x

参考资料

SpringBoot 之 banner 定制

简介

Spring Boot 启动时默认会显示以下 LOGO:

1
2
3
4
5
6
7
  .   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.1.RELEASE)

实际上,Spring Boot 支持自定义 logo 的功能。

让我们来看看如何实现的。

只要你在 resources 目录下放置名为 banner.txtbanner.gifbanner.jpgbanner.png 的文件,Spring Boot 会自动加载,将其作为启动时打印的 logo。

  • 对于文本文件,Spring Boot 会将其直接输出。
  • 对于图像文件( banner.gifbanner.jpgbanner.png ),Spring Boot 会将图像转为 ASCII 字符,然后输出。

变量

banner.txt 文件中还可以使用变量来设置字体、颜色、版本号。

变量 描述
${application.version} MANIFEST.MF 中定义的版本。如:1.0
${application.formatted-version} MANIFEST.MF 中定义的版本,并添加一个 v 前缀。如:v1.0
${spring-boot.version} Spring Boot 版本。如:2.1.1.RELEASE.
${spring-boot.formatted-version} Spring Boot 版本,并添加一个 v 前缀。如:v2.1.1.RELEASE
${Ansi.NAME} (or ${AnsiColor.NAME}, ${AnsiBackground.NAME}, ${AnsiStyle.NAME}) ANSI 颜色、字体。更多细节,参考:AnsiPropertySource
${application.title} MANIFEST.MF 中定义的应用名。

示例:

在 Spring Boot 项目中的 resources 目录下添加一个名为 banner.txt 的文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
${AnsiColor.BRIGHT_YELLOW}${AnsiStyle.BOLD}
________ ___ ___ ________ ___ __ ___ ___
|\ ___ \|\ \|\ \|\ ___ \|\ \ |\ \|\ \|\ \
\ \ \_|\ \ \ \\\ \ \ \\ \ \ \ \ \ \ \ \ \\\ \
\ \ \ \\ \ \ \\\ \ \ \\ \ \ \ \ __\ \ \ \ \\\ \
\ \ \_\\ \ \ \\\ \ \ \\ \ \ \ \|\__\_\ \ \ \\\ \
\ \_______\ \_______\ \__\\ \__\ \____________\ \_______\
\|_______|\|_______|\|__| \|__|\|____________|\|_______|
${AnsiBackground.WHITE}${AnsiColor.RED}${AnsiStyle.UNDERLINE}
:: Spring Boot :: (v${spring-boot.version})
:: Spring Boot Tutorial :: (v1.0.0)

注:${} 设置字体颜色的变量之间不能换行或空格分隔,否则会导致除最后一个变量外,都不生效。

启动应用后,控制台将打印如下 logo:

img
推荐两个生成字符画的网站,可以将生成的字符串放入这个banner.txt 文件:

配置

application.properties 中与 Banner 相关的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# banner 模式。有三种模式:console/log/off
# console 打印到控制台(通过 System.out)
# log - 打印到日志中
# off - 关闭打印
spring.main.banner-mode = off
# banner 文件编码
spring.banner.charset = UTF-8
# banner 文本文件路径
spring.banner.location = classpath:banner.txt
# banner 图像文件路径(可以选择 png,jpg,gif 文件)
spring.banner.image.location = classpath:banner.gif
used).
# 图像 banner 的宽度(字符数)
spring.banner.image.width = 76
# 图像 banner 的高度(字符数)
spring.banner.image.height =
# 图像 banner 的左边界(字符数)
spring.banner.image.margin = 2
# 是否将图像转为黑色控制台主题
spring.banner.image.invert = false

当然,你也可以在 YAML 文件中配置,例如:

1
2
3
spring:
main:
banner-mode: off

编程

默认,Spring Boot 会注册一个 SpringBootBanner 的单例 Bean,用来负责打印 Banner。

如果想完全个人定制 Banner,可以这么做:先实现 org.springframework.boot.Banner#printBanner 接口来自己定制 Banner。在将这个 Banner 通过 SpringApplication.setBanner(…) 方法注入 Spring Boot。

示例源码

示例源码:spring-boot-banner

参考资料

Spring 访问 MongoDB

简介

MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。MongoDB 将数据存储为一个文档,数据结构由键值对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

在 Spring 中,spring-data-mongodb 项目对访问 MongoDB 进行了 API 封装,提供了便捷的访问方式。 Spring Data MongoDB 的核心是一个以 POJO 为中心的模型,用于与 MongoDB DBCollection 交互并轻松编写 Repository 样式的数据访问层。

spring-boot 项目中的子模块 spring-boot-starter-data-mongodb 基于 spring-data-mongodb 项目,做了二次封装,大大简化了 MongoDB 的相关配置。

Spring Boot 快速入门

引入依赖

在 pom.xml 中引入依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

数据源配置

1
2
3
4
5
spring.data.mongodb.host = localhost
spring.data.mongodb.port = 27017
spring.data.mongodb.database = test
spring.data.mongodb.username = root
spring.data.mongodb.password = root

定义实体

定义一个具有三个属性的 Customer 类:idfirstNamelastName

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.data.annotation.Id;

public class Customer {

@Id
public String id;

public String firstName;

public String lastName;

public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

@Override
public String toString() {
return String.format(
"Customer[id=%s, firstName='%s', lastName='%s']",
id, firstName, lastName);
}

}

spring-data-mongodb 会将 Customer 类映射到一个名为 customer 的集合中。如果要更改集合的名称,可以在类上使用 @Document 注解。

创建 Repository

spring-data-mongodb 继承了 Spring Data Commons 项目的能力,所以可以使用其通用 API——Repository

先定义一个 CustomerRepository 类,继承 MongoRepository 接口,并指定其泛型参数:CustomerString。MongoRepository 接口支持多种操作,包括 CRUD 和分页查询。在下面的例子中,定义了两个查询方法:

1
2
3
4
5
6
7
8
9
10
import java.util.List;

import org.springframework.data.mongodb.repository.MongoRepository;

public interface CustomerRepository extends MongoRepository<Customer, String> {

Customer findByFirstName(String firstName);
List<Customer> findByLastName(String lastName);

}

创建 Application

创建一个 Spring Boot 的启动类 Application,并在启动的 main 方法中使用 CustomerRepository 实例访问 MongoDB。

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
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DataMongodbApplication implements CommandLineRunner {

@Autowired
private CustomerRepository repository;

public static void main(String[] args) {
SpringApplication.run(DataMongodbApplication.class, args);
}

@Override
public void run(String... args) {

repository.deleteAll();

// save a couple of customers
repository.save(new Customer("Alice", "Smith"));
repository.save(new Customer("Bob", "Smith"));

// fetch all customers
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : repository.findAll()) {
System.out.println(customer);
}
System.out.println();

// fetch an individual customer
System.out.println("Customer found with findByFirstName('Alice'):");
System.out.println("--------------------------------");
System.out.println(repository.findByFirstName("Alice"));

System.out.println("Customers found with findByLastName('Smith'):");
System.out.println("--------------------------------");
for (Customer customer : repository.findByLastName("Smith")) {
System.out.println(customer);
}
}

}

运行 DataMongodbApplication 的 main 方法后,输出类似如下类容:

1
2
3
4
5
6
7
8
9
10
11
12
Customers found with findAll():
-------------------------------
Customer(id=63d6157b265e7c5e48077f63, firstName=Alice, lastName=Smith)
Customer(id=63d6157b265e7c5e48077f64, firstName=Bob, lastName=Smith)

Customer found with findByFirstName('Alice'):
--------------------------------
Customer(id=63d6157b265e7c5e48077f63, firstName=Alice, lastName=Smith)
Customers found with findByLastName('Smith'):
--------------------------------
Customer(id=63d6157b265e7c5e48077f63, firstName=Alice, lastName=Smith)
Customer(id=63d6157b265e7c5e48077f64, firstName=Bob, lastName=Smith)

示例源码

更多 Spring 访问 MongoDB 示例请参考:MongoDB 示例源码

参考资料

技术文档规范

文档采用 Markdown 语法书写。

📚 “参考”Markdown 语法可以参考:

1. 标题

1.1. 标题层级

标题分为四级。

  1. 一级标题:文章的标题
  2. 二级标题:文章内容的大标题
  3. 三级标题:二级标题下一级的标题
  4. 四级标题:三级标题下一级的标题

1.2. 标题原则

  • 一篇文章中应该尽力避免同名标题。
  • 一级标题下,不能直接出现三级标题。
  • 标题要避免孤立编号(即同级标题只有一个)。
  • 下级标题不重复上一级标题的内容。
  • 谨慎使用四级标题,尽量避免出现,保持层级的简单和防止出现过于复杂的章节。如果三级标题下有并列性的内容,建议只使用项目列表(Item list)。

2. 文本

2.1. 字间距

全角中文字符与半角英文字符之间,应有一个半角空格。

1
2
3
反例:本文介绍如何快速启动Windows系统。

正例:本文介绍如何快速启动 Windows 系统。

全角中文字符与半角阿拉伯数字之间,有没有半角空格都可,但必须保证风格统一,不能两种风格混杂。

1
2
3
正例:2011年5月15日,我订购了5台笔记本电脑与10台平板电脑。

正例:2011 年 5 15 日,我订购了 5 台笔记本电脑与 10 台平板电脑。

半角的百分号,视同阿拉伯数字。

英文单位若不翻译,单位前的阿拉伯数字与单位间不留空格。

1
2
3
反例:一部容量为 16 GB 的智能手机

正例:一部容量为 16GB 的智能手机

半角英文字符和半角阿拉伯数字,与全角标点符号之间不留空格。

1
2
3
反例:他的电脑是 MacBook Air 。

正例:他的电脑是 MacBook Air。

2.2. 句子

  • 避免使用长句。一个句子建议不超过 100 字或者正文的 3 行。
  • 尽量使用简单句和并列句,避免使用复合句。

2.3. 写作风格

尽量不使用被动语态,改为使用主动语态。

1
2
3
反例:假如此软件尚未被安装,

正例:假如尚未安装这个软件,

不使用非正式的语言风格。

1
2
3
反例:Lady Gaga 的演唱会真是酷毙了,从没看过这么给力的表演!!!

正例:无法参加本次活动,我深感遗憾。

用对“的”、“地”、“得”。

1
2
3
4
5
6
7
8
她露出了开心的笑容。
(形容词+的+名词)

她开心地笑了。
(副词+地+动词)

她笑得很开心。
(动词+得+副词)

使用代词时(比如“其”、“该”、“此”、“这”等词),必须明确指代的内容,保证只有一个含义。

1
2
3
反例:从管理系统可以监视中继系统和受其直接控制的分配系统。

正例:从管理系统可以监视两个系统:中继系统和受中继系统直接控制的分配系统。

名词前不要使用过多的形容词。

1
2
3
反例:此设备的使用必须在接受过本公司举办的正式的设备培训的技师的指导下进行。

正例:此设备必须在技师的指导下使用,且指导技师必须接受过由本公司举办的正式设备培训。

单个句子的长度尽量保持在 20 个字以内;20 ~ 29 个字的句子,可以接受;30 ~ 39 个字的句子,语义必须明确,才能接受;多于 40 个字的句子,在任何情况下都不能接受。

1
2
3
反例:本产品适用于从由一台服务器进行动作控制的单一节点结构到由多台服务器进行动作控制的并行处理程序结构等多种体系结构。

正例:本产品适用于多种体系结构。无论是由一台服务器(单一节点结构),还是由多台服务器(并行处理结构)进行动作控制,均可以使用本产品。

同样一个意思,尽量使用肯定句表达,不使用否定句表达。

1
2
3
反例:请确认没有接通装置的电源。

正例:请确认装置的电源已关闭。

避免使用双重否定句。

1
2
3
反例:没有删除权限的用户,不能删除此文件。

正例:用户必须拥有删除权限,才能删除此文件。

2.4. 英文处理

英文原文如果使用了复数形式,翻译成中文时,应该将其还原为单数形式。

1
2
3
英文:⋯information stored in random access memory (RAMs)⋯

中文:……存储在随机存取存储器(RAM)里的信息……

外文缩写可以使用半角圆点(.)表示缩写。

1
2
U.S.A.
Apple, Inc.

表示中文时,英文省略号()应改为中文省略号(……)。

1
2
3
英文:5 minutes later

中文:5 分钟过去了⋯⋯

英文书名或电影名改用中文表达时,双引号应改为书名号。

1
2
3
英文:He published an article entitled "The Future of the Aviation".

中文:他发表了一篇名为《航空业的未来》的文章。

第一次出现英文词汇时,在括号中给出中文标注。此后再次出现时,直接使用英文缩写即可。

1
IOC(International Olympic Committee,国际奥林匹克委员会)。这样定义后,便可以直接使用“IOC”了。

专有名词中每个词第一个字母均应大写,非专有名词则不需要大写。

1
2
3
“American Association of Physicists in Medicine”(美国医学物理学家协会)是专有名词,需要大写。

“online transaction processing”(在线事务处理)不是专有名词,不应大写。

3. 段落

3.1. 段落原则

  • 一个段落只能有一个主题,或一个中心句子。
  • 段落的中心句子放在段首,对全段内容进行概述。后面陈述的句子为核心句服务。
  • 一个段落的长度不能超过七行,最佳段落长度小于等于四行。
  • 段落的句子语气要使用陈述和肯定语气,避免使用感叹语气。
  • 段落之间使用一个空行隔开。
  • 段落开头不要留出空白字符。

3.2. 引用

引用第三方内容时,应注明出处。

1
One man’s constant is another man’s variable. — Alan Perlis

如果是全篇转载,请在全文开头显著位置注明作者和出处,并链接至原文。

1
本文转载自 WikiQuote

使用外部图片时,必须在图片下方或文末标明来源。

1
本文部分图片来自 Wikipedia

3.3. 强调

一些特殊的强调内容可以按照如下方式书写:

🔔 “注意”

💡 “提示”

📚 “参考”

4. 数值

4.1. 半角数字

数字一律使用半角形式,不得使用全角形式。

1
2
3
反例: 这件商品的价格是1000元。

正例: 这件商品的价格是 1000 元。

4.2. 千分号

数值为千位以上,应添加千分号(半角逗号)。

1
XXX 公司的实收资本为 RMB1,258,000

对于 4 ~ 6 位的数值,千分号是选用的,比如10001,000都可以接受。对于 7 位及以上的数值,千分号是必须的。

多位小数要从小数点后从左向右添加千分号,比如4.234,345

4.3. 货币

货币应为阿拉伯数字,并在数字前写出货币符号,或在数字后写出货币中文名称。

1
2
$1,000
1,000 美元

4.4. 数值范围

表示数值范围时,用连接。参见《标点符号》一节的“连接号”部分。

带有单位或百分号时,两个数字都要加上单位或百分号,不能只加后面一个。

1
2
3
4
5
反例:132234kg
正例:132kg~234kg

反例:6789%
正例:67%~89%

4.5. 变化程度的表示法

数字的增加要使用“增加了”、“增加到”。“了”表示增量,“到”表示定量。

1
2
3
4
5
增加到过去的两倍
(过去为一,现在为二)

增加了两倍
(过去为一,现在为三)

数字的减少要使用“降低了”、“降低到”。“了”表示增量,“到”表示定量。

1
2
3
4
5
降低到百分之八十
(定额是一百,现在是八十)

降低了百分之八十
(原来是一百,现在是二十)

不能用“降低 N 倍”或“减少 N 倍”的表示法,要用“降低百分之几”或“减少百分之几”。因为减少(或降低)一倍表示数值原来为一百,现在等于零。

5. 符号

5.1. 符号原则

  • 中文语句的标点符号,均应该采取全角符号,这样可以保证视觉的一致。
  • 如果整句为英文,则该句使用英文/半角标点。
  • 句号、问号、叹号、逗号、顿号、分号和冒号不得出现在一行之首。

5.2. 句号

中文语句中的结尾处应该用全角句号()。

句子末尾用括号加注时,句号应在括号之外。

1
2
3
反例:关于文件的输出,请参照第 1.3 节(见第 26 页。)

正例:关于文件的输出,请参照第 1.3 节(见第 26 页)。

5.3. 逗号

逗号表示句子内部的一般性停顿。

注意避免“一逗到底”,即整个段落除了结尾,全部停顿都使用逗号。

5.4. 顿号

句子内部的并列词,应该用全角顿号() 分隔,而不用逗号,即使并列词是英语也是如此。

1
2
3
反例:我最欣赏的科技公司有 Google, Facebook, 腾讯, 阿里和百度等。

正例:我最欣赏的科技公司有 Google、Facebook、腾讯、阿里和百度等。

英文句子中,并列词语之间使用半角逗号(,)分隔。

1
例句:Microsoft Office includes Word, Excel, PowerPoint, Outlook and other components.

5.5. 分号

分号表示复句内部并列分句之间的停顿。

5.6. 引号

引用时,应该使用全角双引号(“ ”),注意前后双引号不同。

1
例句:许多人都认为客户服务的核心是“友好”和“专业”。

引号里面还要用引号时,外面一层用双引号,里面一层用单引号(‘ ’),注意前后单引号不同。

1
例句:鲍勃解释道:“我要放音乐,可萨利说,‘不行!’。”

5.7. 圆括号

补充说明时,使用全角圆括号(),括号前后不加空格。

1
例句:请确认所有的连接(电缆和接插件)均安装牢固。

5.8. 冒号

全角冒号()常用在需要解释的词语后边,引出解释和说明。

1
例句:请确认以下几项内容:时间、地点、活动名称,以及来宾数量。

表示时间时,应使用半角冒号(:)。

1
例句:早上 8:00

5.9. 省略号

省略号……表示语句未完、或者语气的不连续。它占两个汉字空间、包含六个省略点,不要使用。。。...等非标准形式。

省略号不应与“等”这个词一起使用。

1
2
3
4
5
反例:我们为会餐准备了香蕉、苹果、梨…等各色水果。

正例:我们为会餐准备了各色水果,有香蕉、苹果、梨……

正例:我们为会餐准备了香蕉、苹果、梨等各色水果。

5.10. 感叹号

应该使用平静的语气叙述,尽量避免使用感叹号

不得多个感叹号连用,比如!!!!!

5.11. 破折号

破折号————一般用于做进一步解释。破折号应占两个汉字的位置。

1
例句:直觉————尽管它并不总是可靠的————告诉我,这事可能出了些问题。

5.12. 连接号

连接号用于连接两个类似的词。

以下场合应该使用直线连接号(-),占一个半角字符的位置。

  • 两个名词的复合
  • 图表编号
1
2
3
例句:氧化-还原反应

例句:图 1-1

以下场合应该使用波浪连接号(),占一个全角字符的位置。

  • 数值范围(例如日期、时间或数字)
1
例句:2009 年~2011 年

注意,波浪连接号前后两个值都应该加上单位。

波浪连接号也可以用汉字“至”代替。

1
例句:周围温度:-20°C-10°C

6. 结构

6.1. 目录结构

技术手册目录结构是一部完整的书,建议采用下面的结构。

  • 简介(Introduction) - [必选][目录|文件] 提供对产品和文档本身的总体的、扼要的说明
  • 入门篇(Quickstart) - [可选][文件] 如何最快速地使用产品
  • 基础篇(Basics) - [必选][目录] 又称”使用篇“,提供初级的使用教程
    • 环境准备(Prerequisite) - [可选][文件] 软件使用需要满足的前置条件
    • 安装(Installation) - [可选][文件] 软件的安装方法
    • 配置(Configuration) - [可选][目录|文件] 软件的配置
    • 特性(Feature) - [必选][目录|文件] 软件的功能特性
  • 进阶篇(Advanced) - [可选][目录] 又称”开发篇“,提供中高级的开发教程
    • 原理(Principle) - [可选][目录|文件] 软件的原理
    • 设计(Design) - [可选][目录|文件] 软件的设计,如:架构、设计思想等
  • 实战篇(Action) - [可选][目录] 提供一些具有实战意义的示例说明
  • API(API) - [可选][目录|文件] 软件 API 的逐一介绍
  • 常见问题(FAQ) - [可选][目录|文件] 常见问题解答
  • 附录(Appendix) - [可选][目录] 不属于教程本身、但对阅读教程有帮助的内容
    • 命令(Command) - [可选][目录] 命令
    • 资源(Resource) - [必选][文件] 资源
    • 术语(Glossary) - [可选][文件] 名词解释
    • 技巧(Recipe) - [可选][文件] 最佳实践
    • 版本(Changelog) - [可选][文件] 版本说明
    • 反馈(Feedback) - [可选][文件] 反馈方式

下面是两个真实范例,可参考。

6.2. 文件名

文档的文件名不得含有空格。

文件名必须使用半角字符,不得使用全角字符。这也意味着,中文不能用于文件名。

1
2
3
反例: 名词解释.md

正例: glossary.md

文件名建议只使用小写字母,不使用大写字母。

1
2
3
反例:TroubleShooting.md

正例:troubleshooting.md

为了醒目,某些说明文件的文件名,可以使用大写字母,比如READMELICENSE

文件名包含多个单词时,单词之间建议使用半角的连词线(-)分隔。

1
2
3
反例:advanced_usage.md

正例:advanced-usage.md

7. Emoji

在 markdown 文档中,普遍会使用 emoji,帮助理解内容。但是,如果滥用 emoji,可能会适得其反。

这里,将一些比较约定俗成的 emoji 表情使用场景列举一下:

  • 💡 提示 - [推荐]
  • 🔔 注意、警告 - [推荐]
  • ⭕ 正确 - [推荐]
  • ❌ 错误 - [推荐]
  • ❓ 问题 - [推荐]
  • ⛔ 禁止 - [推荐]
  • 🚧 未完待续、有待补充 - [推荐]
  • 📚 参考、参考资料 - [可选]
  • ⌨ 源码 - [可选]

8. 参考

个人目录管理规范

作为程序员,想必每个人都会有大量的资料、数据。按照条理清晰的目录结构去分类化存储,十分有助于管理文件。

目录结构

以下是我个人整理的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.
├── Codes #代码目录
│ ├── Other #第三方代码目录
│ ├── My #个人代码目录
│ └── Work #工作代码目录
├── Data #数据目录
├── Downloads #下载文件目录
├── Docs #文档目录
│ ├── Books #电子书目录
│ ├── My #个人文档目录
│ └── Work #工作文档目录
├── Movies #视频目录
├── Music #音乐目录
├── Pictures #图片目录
├── Public #共享目录
├── Temp #临时文件目录
└── Tools #工具软件目录
└── Packages #安装包目录

注:如果您使用的操作系统是 Mac 这种可以为目录或文件添加 tag 的操作系统,那么您可以根据自己的喜好更细致化的管理。

2. 文件管理软件

选用便利的文件管理软件,可以让你的文件管理如虎添翼。这里推荐几款经典的文件管理工具。

2.1. Clover

Clover 是 Windows Explorer 资源管理器的一个扩展,为其增加类似谷歌 Chrome 浏览器的多标签页功能。

2.2. Everything

Everything 可以立即在 windows 系统中找到制定名称的文件和文件夹。

2.3. Wox

Wox 是一款简单易用的 Windows 启动器。可以把它视为 windows 版的 Alfred。

2.4. Q-dir

Q-dir 是轻量的文件管理器,特点鲜明,各种布局视图切换灵活,默认四个小窗口组成一个大窗口,操作快捷。

Maven 实战问题和最佳实践

Maven 常见问题

dependencies 和 dependencyManagement,plugins 和 pluginManagement 有什么区别

dependencyManagement 是表示依赖 jar 包的声明,即你在项目中的 dependencyManagement 下声明了依赖,maven 不会加载该依赖,dependencyManagement 声明可以被继承。

dependencyManagement 的一个使用案例是当有父子项目的时候,父项目中可以利用 dependencyManagement 声明子项目中需要用到的依赖 jar 包,之后,当某个或者某几个子项目需要加载该插件的时候,就可以在子项目中 dependencies 节点只配置 groupId 和 artifactId 就可以完成插件的引用。

dependencyManagement 主要是为了统一管理插件,确保所有子项目使用的插件版本保持一致,类似的还有 plugins 和 pluginManagement。

IDEA 修改 JDK 版本后编译报错

错误现象:

修改 JDK 版本,指定 maven-compiler-plugin 的 source 和 target 为 1.8 。

然后,在 Intellij IDEA 中执行 maven 指令,报错:

1
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.0:compile (default-compile) on project apollo-common: Fatal error compiling: 无效的目标版本: 1.8 -> [Help 1]

错误原因:

maven 的 JDK 源与指定的 JDK 编译版本不符。

排错手段:

  • 查看 Project Settings

Project SDK 是否正确

img

SDK 路径是否正确

img

  • 查看 Settings > Maven 的配置

JDK for importer 是否正确

img

Runner 是否正确

img

重复引入依赖

在 Idea 中,选中 Module,使用 Ctrl+Alt+Shift+U,打开依赖图,检索是否存在重复引用的情况。如果存在重复引用,可以将多余的引用删除。

如何打包一个可以直接运行的 Spring Boot jar 包

可以使用 spring-boot-maven-plugin 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

如果引入了第三方 jar 包,如何打包?

首先,要添加依赖

1
2
3
4
5
6
7
<dependency>
<groupId>io.github.dunwu</groupId>
<artifactId>dunwu-common</artifactId>
<version>1.0.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/dunwu-common-1.0.0.jar</systemPath>
</dependency>

接着,需要配置 spring-boot-maven-plugin 插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
</build>

去哪儿找 maven dependency

问:刚接触 maven 的新手,往往会有这样的疑问,我该去哪儿找 jar?

答:官方推荐的搜索 maven dependency 网址:

如何指定编码

问:众所周知,不同编码格式常常会产生意想不到的诡异问题,那么 maven 构建时如何指定 maven 构建时的编码?

答:在 properties 中指定 project.build.sourceEncoding

1
2
3
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

如何指定 JDK 版本

问:如何指定 maven 构建时的 JDK 版本

答:有两种方法:

(1)properties 方式

1
2
3
4
5
6
7
8
<project>
...
<properties>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
...
</project>

(2)使用 maven-compiler-plugin 插件,并指定 source 和 target 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<build>
...
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
...
</build>

如何避免将 dependency 打包到构件中

答:指定 maven dependency 的 scope 为 provided,这意味着:依赖关系将在运行时由其容器或 JDK 提供。
具有此范围的依赖关系不会传递,也不会捆绑在诸如 WAR 之类的包中,也不会包含在运行时类路径中。

如何跳过单元测试

问:执行 mvn package 或 mvn install 时,会自动编译所有单元测试(src/test/java 目录下的代码),如何跳过这一步?

答:在执行命令的后面,添加命令行参数 -Dmaven.test.skip=true 或者 -DskipTests=true

如何引入本地 jar

问:有时候,需要引入在中央仓库找不到的 jar,但又想通过 maven 进行管理,那么应该如何做到呢?
答:可以通过设置 dependency 的 scope 为 system 来引入本地 jar。
例:

  • 将私有 jar 放置在 resouces/lib 下,然后以如下方式添加依赖:
  • groupId 和 artifactId 可以按照 jar 包中的 package 设置,只要和其他 jar 不冲突即可。
1
2
3
4
5
6
7
<dependency>
<groupId>xxx</groupId>
<artifactId>xxx</artifactId>
<version>1.0.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/xxx-6.0.0.jar</systemPath>
</dependency>

如何排除依赖

问:如何排除依赖一个依赖关系?比方项目中使用的 libA 依赖某个库的 1.0 版。libB 以来某个库的 2.0 版,如今想统一使用 2.0 版,怎样去掉 1.0 版的依赖?

答:通过 exclusion 排除指定依赖即可。

例:

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.12</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>

Maven 最佳实践

通过 bom 统一管理版本

采用类似 spring-boot-dependencies 的方式统一管理依赖版本。

spring-boot-dependencies 的 pom.xml 形式:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.9.RELEASE</version>
<packaging>pom</packaging>

<!-- 省略 -->

<!-- 依赖包版本管理 -->
<dependencyManagement>
<dependencies>
<!-- 省略 -->
</dependencies>
</dependencyManagement>

<build>
<!-- 插件版本管理 -->
<pluginManagement>
<plugins>
<!-- 省略 -->
</plugins>
</pluginManagement>
</build>
</project>

其他项目引入 spring-boot-dependencies 来管理依赖版本的方式:

1
2
3
4
5
6
7
8
9
10
11
 <dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

UML 结构建模图

结构图定义了一个模型的静态架构。它们通常被用来对那些构成模型的‘要素’建模,诸如:类,对象,接口和物理组件。另外,它们也被用来对元素间关联和依赖关系进行建模。

关键词:部署图, 组件图, 包图, 类图, 复合结构图, 对象图

部署图

部署图(Deployment Diagram)用于对系统的物理结构建模。部署图将显示系统中的软件组件和硬件组件之间的关系以及处理工作的物理分布。

节点

节点既可以是硬件元素,也可以是软件元素。它显示为一个立方体,如下图所示。

节点实例

图可以显示节点实例,实例与节点的区分是:实例的名称带下划线,冒号放在它的基本节点类型之前。实例在冒号之前可以有名称,也可以没有名称。下图显示了一个具名的计算机实例。

节点构造型

为节点提供了许多标准的构造型,分别命名为 «cdrom», «cd-rom», «computer», «disk array», «pc», «pc client», «pc server», «secure», «server», «storage», «unix server», «user pc»。 并在节点符号的右上角显示适当的图标。

工件

工件是软件开发过程中的产品。包括过程模型(如:用例模型,设计模型等),源文件,执行文件,设计文档,测试报告,构造型,用户手册等等。

工件表示为带有工件名称的矩形,并显示«artifact»关键字和文档符号。

关联

在部署图的上下文联系中,关联代表节点间的联系通道。下图显示了一个网络系统的部署图,描述了网络协议为构造型和关联终端的多重性,

作为容器的节点

节点可以包含其他元素,如组件和工件。下图显示了一个嵌入式系统某个部分的部署图。描写了一个被主板节点包含的可执行工件。

组件图

组件图(Component Diagram)描绘了组成一个软件系统的模块和嵌入控件。组件图比类图具有更高层次的抽象-通常运行时一个组件被一个或多个类(或对象)实现。它们象积木那样使得组件能最终构成系统的绝大部分。

上图演示了一些组件和它们的内部关系。装配连接器(Assembly connectors)“连接”由”Product”和”Customer”的提供接口到由 “Order”指定的需求接口。 一个依赖关系映射了客户相关的帐户信息到“Order”需要的 “Payment”需求接口。

实际上,组件图同包图很相似,它们都有明确的界限,把元素分组到逻辑结构中。他们之间的不同是:组件图提供了语义更丰富的分组机制,在组件图中,所有的模型元素都是私有的,而包图只显示公有的成员。

表现组件

组件可表示为带关键字 «component»的矩形类元;也可用右上角有组件图标的矩形表示。

装配连接器

装配连接器在组件 “Component1”的需求接口和另一个组件 “Component2”的提供接口之间建立桥梁; 这个桥梁使得一个组件能提供另一个组件所需要的服务。

带端口组件

使用端口的组件图允许在它的环境指定一个服务和行为,同时这个服务和行为也是组件需要的。当端口进行双向操作的时候,它可以指定输入和输出。下图详述了用于在线服务的带端口组件,它有两个提供接口 “order entry”和 “tracking”,也有 “payment” 需求接口。

包图

包图(Package Diagram)用来表现包和它所包含元素的组织。当用来代表类元素时,包图提供了命名空间的可视化。包图最常用的用途是用来组织用例图和类图,尽管它不局限于这些 UML 元素。

下面是一个包图的例子。

包中的元素共享相同的命名空间,因此,一个指定命名空间的元素必须有唯一的名称。

包可以用来代表物理或逻辑关系。选择把类包括在指定的包里,有助于在同一个包里赋予这些类相同继承层次。通常认为把通过复合相关联的类,以及与它们相协作的类放在同一个包里。

在 UML2.5 中,包用文件夹来表示,包中的元素共享同一个命名空间,并且必须是可识别的,因此要有唯一的名称或类型。包必须显示包名,在附属方框部分有选择的显示包内的元素。

  • 包的合并 - 包之间的合并连接符«merge»定义了源包元素与目标包同名元素之间的泛化关系。源包元素的定义被扩展来包含目标包元素定义。当源包元素与目标包内没有同名元素时,目标包元素的定义不受影响。
  • 包的导入 - 导入连接符 «import»表明目标包的元素,在该例中是一个类 ,在源包中被引用要用非限定修饰名。源包的命名空间获得目标类的接口,目标包的命名空间则不受影响。
  • 嵌套连接符 - 源包和目标包间的嵌套连接符说明目标包完全包含源包。

类图

类图(Class Diagram)展示了面向对象系统的构造模块。描绘了模型或部分模型的静态视图,显示它包含的属性和行为,而不是详细描述操作的功能或完善方法。类图最常用来表达多个类和接口之间的关系。泛化(Generalizations),聚合(aggregations)和关联(associations)分别是类之间继承,复合或应用,及连接的表现。

下面的图显示了类之间的聚合关系。弱聚合(浅色箭头)表现在类 “Account” 使用 “AddressBook”,但是不必要包含它的一个实例。强聚合(图中的黑色箭头)表示了目标类包含源类,例如,”Contact” 和 “ContactGroup”值被包含在 “AddressBook”中。

类(Classes)

类是定义对象所具有的属性和行为的元素。行为用类能理解的合适消息和适合每条消息的操作来描述。 类中也可能定义约束,标记值,构造型。

类的标柱(Class Notation)

类用矩形表示。除类的名称外,还可以选择性地显示属性和操作。 分栏分别用来显示类的名称,属性和操作。

在下面图中,类的类名显示在最上面的分栏,它下面的分栏显示详细属性,如:”center” 属性显示初始化的值。最后面的分栏显示操作,如: setWidth,setLength 和 setPosition 以及他们的参数。 属性和操作名前的标注表示了该属性或操作的可见性: 如果使用 “+”号,这个属性或操作是公共的 ; “-“ 号则代表这个属性或操作是私有的。 “#”号是这个属性或操作被定义为保护的,” ~“ 号代表包的可见性。

接口(Interfaces)

接口是实施者同意满足的行为规范,是一种约定。实现一个接口,类必需支持其要求的行为,使系统按照同样的方式,即公共的接口,处理不相关的元素。

接口有相似于类的外形风格,含有指定的操作,如下图所示。如果没有明确的详细操作,也可以画成一个圆环。当画成圆环的时候,到这个环形标柱的实现连接没有目标箭头。

表(Tables)

表尽管不是基本 UML 的一部分,仍然是“图型”能完成的实例用。在右上角画一个表的小图标来表示。表属性用“图型” «column»表示。 绝大多数表单有一个主键,是由一个或几个字段组成的一个唯一的字码组合加主键操作来访问表格,主键操作“图型”为«PK»。 一些表有一个或多个外键,使用一个或多个字段加一个外键操作,映射到相关表的主键上去,外键操作“图型”为«FK»。

关联(Associations)

关联表明两个模型元素之间有关系,通常用在一个类中被实现为一个实例变量。连接符可以包含两端的命名的角色,基数性,方向和约束。关联是元素之间普通的关系。如果多于两个元素,也可以使用菱形的关联关系。当从类图生成代码时,关联末端的对象将变成目标类中实例变量。见下图示例 “playsFor” 将变成”Player”类中的实例变量。

泛化(Generalizations)

泛化被用来说明继承关系。连接从特定类元到一般类元。泛化的含义是源类继承了目标类的特性。下图的图显示了一个父类泛化一个子类, 类“Circle”的一个实例将会有属性 “ x_position”,“ y_position” , “radius” 和 方法 “display()”。 注意:类 “Shape” 是抽象的,类名显示为斜体。

下图显示了与上图相同信息的视图。

聚合(Aggregations)

聚合通常被用来描述由更小的组件所构成的元素。聚合关系表示为白色菱形箭头指向目标类或父类。

聚合的更强形式 -组合聚合(强聚合) - 显示为黑色菱形箭头,用来组合每次最大化的包含组件。如果一个组合聚合的父类被删除,通常与他相关的所有部分都会被删除,但是,如果一个部件从组合中去掉,将不用删除整个组合。组合是可迁,非对称的关系和递归的。

下面的图示:显示了弱聚合和强聚合的不同。“ address book” 由许多 “contacts” 和 “contact groups”组成。 “contact group” 是一个“contacts”的虚分组; “contact”可以被包含在不止一个 “ contact group”。 如果你删除一个“ address book”,所有的 “contacts” 和 “contact groups” 也将会被删除;如果你删除“ contact group”, 没有 “contacts”会被删除。

关联类(Association Classes)

关联类是一个允许关联连接有属性和操作的构造。下面的示例:显示了远不止简单连接两个类的连接,如给“employee”分配项目。“ employee”在项目中所起的作用是一个复杂的实体,既有自身的也有不属于“employee” 或 “project” 类的细节。 例如,“ employee”可以同时为几个项目工作,有不同的职务头衔和对应的安全权限。

依赖(Dependencies)

依赖被用来描述模型元素间广泛的依赖关系。通常在设计过程早期显示两个元素之间存在某种关系,因为是初期而不能确定具体是什么关系,在设计过程末期,该继承关系会被归入已有构造型 (构造型 可以是实例化 «instantiate»,跟踪 «trace»,导入 «import», 和其它的关系),或被替换成一个更明确类型的连接符。

跟踪(Traces)

跟踪关系是一种特殊化的依赖关系。连接模型元素或跨模型但是具有相同概念的模型元素集。跟踪被经常用来追踪需求和模型的变化。由于变化是双向的,这种依赖关系的顺序通常被忽略。这种关系的属性可以被指定为单向映射,但跟踪是双向的,非正式的和很少可计算的。

实现(Realizations)

是源对象执行或实现目标,实现被用来表达模型的可跟踪性和完整性-业务模型或需求被一个或多个用例实现,用例则被类实现,类被组件实现,等等。这种实现贯穿于系统设计的映射需求和类等,直至抽象建模水平级。从而确保整个系统的一张宏图,它也反映系统的所有微小组成,以及约束和定义它的细节。实现关系用带虚线的实箭头表示。

嵌套(Nestings)

嵌套连接符用来表示源元素嵌套在目标元素中。下图显示“ inner class”的定义,尽管在 EA 中,更多地按照着他们在项目层次视图中的位置来显示这种关系。

复合结构图

复合结构图显示类的内部结构,包括它与系统其他部分的交互点。也显示各部分的配置与关系,这些部分一起执行类元的行为。

类元素已经在类图部分被详细地阐述,这部分用来说明类表现复合元素的方式,如:暴露接口,包含端口和部件。

部件

部件是代表一组(一个或多个)实例的元素,这组实例的拥有者是一类元实例,例如:如果一个图的实例有一组图形元素,则这些图形元素可以被表示为部件,并可以对他们之间的某种关系建模。注意:一个部件可以在它的父类被删除之前从父类中被去掉,这样部件就不会被同时删除了。
部件在类或组件内部显示为不加修饰的方框。

端口

端口是类型化的元素,代表一个包含类元实例的外部可视的部分。端口定义了类元和它的环境之间的交互。端口显示在包含它的部件,类或组合结构的边缘上。端口指定了类元提供的服务,以及类元要求环境提供的服务。
端口显示为所属类元边界指定的方框。

接口

接口与类相似,但是有一些限制,所有的接口操作都是公共和抽象的,不提供任何默认的实现。所有的接口属性都必须是常量。然而,当一个类从一个单独的超级类继承而来,它可以实现多个接口。
当一个接口在图中单列出来,它既可以显示为类元素的方框,带 «interface» 关键字和表明它是抽象的斜体名称,也可以显示为圆环。

注意:圆环标注不显示接口操作。当接口显示为类所有的接口,它们会被当作暴露接口引用。暴露接口可以定义为是提供的,还是需求的。提供接口确认包含它的类元提供指定接口元素定义的操作,可通过类和接口间实现的连接来定义。需求接口说明该类元能与其他类元进行通信,这些类元提供了指定接口元素所定义的操作。需求接口可通过在类和接口间建立依赖连接来定义。
提供接口显示为“带棒球体”,依附在类元边缘。需求接口显示为“带棒杯体”,也是依附在类元边缘。

委托

委托连接器用来定义组件外部端口和接口的内部工作方式。委托连接器表示为带有 «delegate» 关键字的箭头。它连接组件的外部约定,表现为它的端口,到组件部件行为的内部实现。

协作

协作定义了一系列共同协作的角色,它们集体展示一个指定的设计功能。协作图应仅仅显示完成指定任务或功能的角色与属性。隔离主要角色是用来简化结构和澄清行为,也用于重用。一个协作通常实现一个模式。
协作元素显示为椭圆。

角色绑定

角色绑定连接器是一条从连接协作到所要完成该任务类元的连线。它显示为虚线,并在类元端显示作用名。

表现

表现连接器用于连接协作到类元来表示此类元中使用了该协作。显示为带关键字 «represents»的虚线箭头。

发生
发生连接器用于连接协作到类元来表示此协作表现了(同原文)该类元;显示为带关键字«occurrence»的虚线箭头。

对象图

对象图(Object Diagram)可以认为是类图的特殊情形,是类图元素子集,被用来及时强调在某些点,类的实例间的关系。这对理解类图很有帮助。他们在构造上与类图显示没有不同,但是反映出多样性和作用。

类和对象元素

下面的图显示了类元素和对象元素外观上的不同。注意:类元素包括三个部分,分别是名字栏,属性栏和操作栏;对象元素默认为没有分栏。名称显示也有不同:对象名称有下划线,并可能显示该对象实例化所用类元的名称。

运行状态

类元元素可以有任意数量的属性和操作。在对象实例中不会被显示出来。但可能定义对象的运行状态,显示特殊实例的属性设置值。

类和对象图示例

下图是一个对象图,其中插入了类定义图。它例示如何用对象图来测试类图中任务多重性的方法。“car” 类对 “wheel” 类有“1 对多” 的多重性,但是如果已经选择用“1 对 4” 来替代,那样就不会在对象图显示“3 个轮子”的汽车。

参考资料

域名解析协议 DNS

域名系统(英文:Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和 IP 地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS 使用 TCP 和 UDP 端口 53。当前,对于每一级域名长度的限制是 63 个字符,域名总长度则不能超过 253 个字符。

关键词:DNS, 域名解析

简介

什么是 DNS

DNS 是一个应用层协议。

域名系统 (DNS) 的作用是将人类可读的域名 (如,www.example.com) 转换为机器可读的 IP 地址 (如,192.0.2.44)。

什么是域名

域名是由一串用点分隔符 . 组成的互联网上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的方位。域名可以说是一个 IP 地址的代称,目的是为了便于记忆后者。例如,wikipedia.org 是一个域名,和 IP 地址 208.80.152.2 相对应。人们可以直接访问 wikipedia.org 来代替 IP 地址,然后域名系统(DNS)就会将它转化成便于机器识别的 IP 地址。这样,人们只需要记忆 wikipedia.org 这一串带有特殊含义的字符,而不需要记忆没有含义的数字。

DNS 的分层

域名系统是分层次的。

在域名系统的层次结构中,各种域名都隶属于域名系统根域的下级。域名的第一级是顶级域,它包括通用顶级域,例如 .com.net.org;以及国家和地区顶级域,例如 .us.cn.tk。顶级域名下一层是二级域名,一级一级地往下。这些域名向人们提供注册服务,人们可以用它创建公开的互联网资源或运行网站。顶级域名的管理服务由对应的域名注册管理机构(域名注册局)负责,注册服务通常由域名注册商负责。

DNS 服务类型

  • 授权型 DNS - 一种授权型 DNS 服务提供一种更新机制,供开发人员用于管理其公用 DNS 名称。然后,它响应 DNS 查询,将域名转换为 IP 地址,以便计算机可以相互通信。授权型 DNS 对域有最终授权且负责提供递归型 DNS 服务器对 IP 地址信息的响应。Amazon Route 53 是一种授权型 DNS 系统。
  • 递归型 DNS - 客户端通常不会对授权型 DNS 服务直接进行查询。而是通常连接到称为解析程序的其他类型 DNS 服务,或递归型 DNS 服务。递归型 DNS 服务就像是旅馆的门童:尽管没有任何自身的 DNS 记录,但是可充当代表您获得 DNS 信息的中间程序。如果递归型 DNS 拥有已缓存或存储一段时间的 DNS 参考,那么它会通过提供源或 IP 信息来响应 DNS 查询。如果没有,则它会将查询传递到一个或多个授权型 DNS 服务器以查找信息。

记录类型

DNS 中,常见的资源记录类型有:

  • NS 记录(域名服务) ─ 指定解析域名或子域名的 DNS 服务器。
  • MX 记录(邮件交换) ─ 指定接收信息的邮件服务器。
  • A 记录(地址) ─ 指定域名对应的 IPv4 地址记录。
  • AAAA 记录(地址) ─ 指定域名对应的 IPv6 地址记录。
  • CNAME(规范) ─ 一个域名映射到另一个域名或 CNAME 记录( example.com 指向 www.example.com )或映射到一个 A记录。
  • PTR 记录(反向记录) ─ PTR 记录用于定义与 IP 地址相关联的名称。 PTR 记录是 A 或 AAAA 记录的逆。 PTR 记录是唯一的,因为它们以 .arpa 根开始并被委派给 IP 地址的所有者。

详细可以参考:维基百科 - 域名服务器记录类型列表

域名解析

主机名到 IP 地址的映射有两种方式:

  • 静态映射 - 在本机上配置域名和 IP 的映射,旨在本机上使用。Windows 和 Linux 的 hosts 文件中的内容就属于静态映射。
  • 动态映射 - 建立一套域名解析系统(DNS),只在专门的 DNS 服务器上配置主机到 IP 地址的映射,网络上需要使用主机名通信的设备,首先需要到 DNS 服务器查询主机所对应的 IP 地址。

通过域名去查询域名服务器,得到 IP 地址的过程叫做域名解析。在解析域名时,一般先静态域名解析,再动态解析域名。可以将一些常用的域名放入静态域名解析表中,这样可以大大提高域名解析效率。

上图展示了一个动态域名解析的流程,步骤如下:

  1. 用户打开 Web 浏览器,在地址栏中输入 www.example.com,然后按 Enter 键。
  2. www.example.com 的请求被路由到 DNS 解析程序,这一般由用户的 Internet 服务提供商 (ISP) 进行管理,例如有线 Internet 服务提供商、DSL 宽带提供商或公司网络。
  3. ISP 的 DNS 解析程序将 www.example.com 的请求转发到 DNS 根名称服务器。
  4. ISP 的 DNS 解析程序再次转发 www.example.com 的请求,这次转发到 .com 域的一个 TLD 名称服务器。.com 域的名称服务器使用与 example.com 域相关的四个 Amazon Route 53 名称服务器的名称来响应该请求。
  5. ISP 的 DNS 解析程序选择一个 Amazon Route 53 名称服务器,并将 www.example.com 的请求转发到该名称服务器。
  6. Amazon Route 53 名称服务器在 example.com 托管区域中查找 www.example.com 记录,获得相关值,例如,Web 服务器的 IP 地址 (192.0.2.44),并将 IP 地址返回至 DNS 解析程序。
  7. ISP 的 DNS 解析程序最终获得用户需要的 IP 地址。解析程序将此值返回至 Web 浏览器。DNS 解析程序还会将 example.com 的 IP 地址缓存 (存储) 您指定的时长,以便它能够在下次有人浏览 example.com 时更快地作出响应。有关更多信息,请参阅存活期 (TTL)。
  8. Web 浏览器将 www.example.com 的请求发送到从 DNS 解析程序中获得的 IP 地址。这是您的内容所处位置,例如,在 Amazon EC2 实例中或配置为网站终端节点的 Amazon S3 存储桶中运行的 Web 服务器。
  9. 192.0.2.44 上的 Web 服务器或其他资源将 www.example.com 的 Web 页面返回到 Web 浏览器,且 Web 浏览器会显示该页面。

🔔 注意:只有配置了域名服务器,才能执行域名解析。

例如,在 Linux 中执行 vim /etc/resolv.conf 命令,在其中添加下面的内容来配置域名服务器地址:

1
nameserver 218.2.135.1

Linux 上的域名相关命令

hostname

hostname 命令用于查看和设置系统的主机名称。环境变量 HOSTNAME 也保存了当前的主机名。在使用 hostname 命令设置主机名后,系统并不会永久保存新的主机名,重新启动机器之后还是原来的主机名。如果需要永久修改主机名,需要同时修改 /etc/hosts/etc/sysconfig/network 的相关内容。

参考:http://man.linuxde.net/hostname

示例:

1
2
$ hostname
AY1307311912260196fcZ

nslookup

nslookup 命令是常用域名查询工具,就是查 DNS 信息用的命令。

参考:http://man.linuxde.net/nslookup

示例:

1
2
3
4
5
6
7
8
[root@localhost ~]# nslookup www.jsdig.com
Server: 202.96.104.15
Address: 202.96.104.15#53

Non-authoritative answer:
www.jsdig.com canonical name = host.1.jsdig.com.
Name: host.1.jsdig.com
Address: 100.42.212.8

更多内容

UML 行为建模图

行为图用来记录在一个模型内部,随时间的变化,模型执行的交互变化和瞬间的状态;并跟踪系统在真实环境下如何表现,以及观察系统对一个操作或事件的反应,以及它的结果。

关键词:活动图, 状态机图, 用例图, 通信图, 交互概述图, 时序图, 时间图

活动图

UML 中,活动图用来展示活动的顺序。显示了从起始点到终点的工作流,描述了活动图中存在于事件进程的判断路径。活动图可以用来详细阐述某些活动执行中发生并行处理的情况。活动图对业务建模也比较有用,用来详细描述发生在业务活动中的过程。
一个活动图的示例如下所示。

下面描述组成活动图的元素。

活动

活动是行为参数化顺序的规范。活动被表示为圆角矩形,内含全部的动作,工作流和其他组成活动的元素。

动作

一个动作代表活动中的一个步骤。动作用圆角矩形表示。

动作约束

动作可以附带约束,下图显示了一个带前置条件和后置条件的动作。

控制流

控制流显示一个动作到下一个动作的流。表示为带箭头实线

初始节点

一个开始或起始点用大黑圆点表示,如下图。

结束节点

结束节点有两种类型:活动结束节点和流结束节点。活动结束节点表示为中心带黑点的圆环。

流结束节点表示为内部为叉号的圆环。

这两种不同类型节点的区别为:流结束节点表明单独的控制流的终点。活动结束终点是活动图内所有控制流的结束。

对象和对象流

对象流是对象和数据转递的通道。对象显示为矩形。

对象流显示为带箭头的连接器,表明方向和通过的对象。

一个对象流在它的至少一个终端有一个对象。在上图中,可以采用带输入输出引脚的速记标柱表示。

数据存储显示为带 «datastore» 关键字的对象。

判断节点和合并节点

判断节点和合并节点是相同标注:菱形。它们可以被命名。从判断节点出来的控制流有监护条件,当监护条件满足时,可以对流控制。下图显示了判断节点和合并节点的使用。

分叉和结合节点

分叉和结合节点有同样的标柱:垂直或水平条(方向取决于工作流从左到右,还是从上到下)。它们说明了控制的并发线程的起始和终点,下图显示他们的使用示例。

结合节点与合并节点不同之处在于:结合节点同步两个输入量,产生一个单独的输出量。来自结合节点的输出量要接收到所有的输入量后才能执行。合并节点直接将控制流传递通过。如果两个或更多的输入量到达合并节点。则它的输出流指定的动作会被执行两次或更多次。

扩展域

扩展域是会执行多次的结构活动域。输入输出扩展节点表示为一组“3 厢” ,代表多个选择项。关键词 “iterative”, “parallel” 或 “stream”显示在区域的左上角

异常处理器

异常处理器在活动图中可以建模。

可中断活动区

可中断活动区环绕一组可以中断的动作。在下面非常简单的例子中: 当控制被传递到结束订单 “Close Order” 动作,定单处理”Process Order” 动作会执行直到完成,除非”Cancel Request”取消请求中断被接受,这会将控制传递给”Cancel Order”动作。

分割

一个活动分割显示为垂直或水平泳道。在下图中,分割被用来在活动图中分隔动作,有在 “accounting department”中执行的,有在 “customer”中执行的。

状态机图

状态机图(state-machine-diagram)对一个单独对象的行为建模,指明对象在它的整个生命周期里,响应不同事件时,执行相关事件的顺序。

如下示例, 下列的状态机图显示了门在它的整个生命周期里如何运作。

门可以处于以下的三种状态之一: “Opened”打开状态, “Closed”关闭状态,或者”Locked”锁定状态。 它分别响应事件:“Open”开门, “Close”关门, “Lock”锁门 和 “Unlock”解锁。 注意:不是所有的事件,在所有的状态下都是有效的。如:一个门打开的时候是不可能锁定的,除非你关上门。并且,状态转移可能有附加监护条件:假设门是开的,如果“doorWay->isEmpty”(门是空的)被满足,那么它只能响应关门事件。状态机图使用的语法和约定将在下面的部分进行讨论。

状态

状态被表示为圆角矩形,状态名写在里面。

起始和结束状态

初始状态表示为实心黑圆环,可以标注名称。结束状态表示为中心带黑点圆环,也可以被标注名称。

转移

一个状态到下一个状态的转移表示为带箭头实线。转移可以有一个“Trigger”触发器,一个“Guard”监护条件和一个“effect”效果。如下所示:

“Trigger”触发器是转移的起因,它可以是某个条件下的一个信号,一个事件,一个变化或一个时间通路。”Guard”监护是一个条件,而且必须为真,以便于让触发器引起转移。效果”Effect”是直接作用到对象上的一个动作,该对象具有做为转移结果的状态机。

状态活动

在上面的状态转移示例中,一个效果与该转移相关联。如果目标状态有多个转移到达,并且每一个转移都有相同的效果与它相关联,那最好将该效果与目标状态相关联,而不与转移相关联。你可以通过为这个状态定义初始动作来实现。下图显示了一个带入口动作和出口动作的状态。

可以定义发生在事件上的动作或一直发生的动作。每一种类型的动作是可以定义任意数量的。

自转移

一个状态可能有一个返回到自身的转移,如下图。效果与转移关联是十分有帮助。

复合状态

一个状态机图可以有子状态机图,如下图所示:

可选择不同方式显示相同信息,如下图所示:

上面版本的标注说明”Check PIN”的子状态机图显示在单独的图中。

入口点

有时,你不想在正常的初始状态进入子状态机。例如下面的子状态机,它通常从”初始化”状态开始,但是如果因为某些原因,它不必执行初始化,可能靠转移到指定的入口点来从 “Ready” 状态开始。

下图显示了状态机的上一层。

出口点

有与入口点相类似的方式,它可能也指定可选择的出口点。下图给出了主处理状态执行后,所执行状态的去向将取决于该状态转移时所使用的路径。

选择伪状态

选择伪状态显示为菱形,有一个转移输入,两个或多个输出。下图显示不管到达哪一个状态,经过选择伪状态后的去向,取决于在伪状态中执行时所选择的消息格式。

连接伪状态

连接伪状态用来将多个状态转移链接在一起。一个单独的连接伪状态可以有一个或多个输入和一个或多个输出,监护可能应用于每一个转移,连接是没有语义的。连接可以把一个输入转移分成多个输出转移来实现一个静态分支。与之对照的是选择伪状态实现一个动态条件分支。

终止伪状态

进入终止伪状态是指状态机生命线已经终止。终止伪状态表示为叉号。

历史状态

历史状态用来当状态机中断时,恢复状态机之前状态。下面例图说明了历史状态的使用。这个例子是关于洗衣机的状态机。

在这个状态机中,当洗衣机运行时,它会按照”Washing” 到 Rinsing”再到”Spinning”来进行。如果电源被切断 ,洗衣机会停止运行并进入”Power Off” 状态。当电源恢复,运行状态在”History State”符号处进入,表示它会从上次离开的地方恢复。

并发区

一个状态可以被分成几个不同的区,包含同时存在和执行的子状态。下面的例子显示状态 “Applying Brakes”, “front brake”和”rear brakes” 将同时独立运作。注意使用了分叉和结合伪状态而不是选择和合并伪状态。这些符号用来同步并发的线程。

用例图

用例图用来记录系统的需求,它提供系统与用户及其他参与者的一种通信手段。

执行者

用例图显示了系统和系统外实体之间的交互。这些实体被引用为执行者。执行者代表角色,可以包括:用户,外部硬件和其他系统。执行者往往被画成简笔画小人。也可以用带«actor»关键字的类矩形表示。

在下图中,执行者可以详细的泛化其他执行者:

用例

用例是有意义的单独工作单元。它向系统外部的人或事提供一个易于观察的高层次行为视图。 用例的标注符号是一个椭圆。

使用用例的符号是带可选择箭头的连接线,箭头显示控制的方向。下图说明执行者 “Customer”使用 “Withdraw”用例。

用途连接器(uses connector)可以有选择性的在每一个端点有多重性值,如下图,显示客户一次可能只执行一次取款交易。但是银行可以同时执行许多取款交易。

用例定义

一个典型的用例包括:

  • 名称和描述 - 用例通常用一个动词词组定义,而且有一个简短的文字说明。
  • 需求 - 需求定义了一个用例必须提供给终端用户的正式功能性需求。它们符合构造方法建立的功能性规范。一个需求是用例将执行一个动作或提供多个值给系统的约定或承诺。
  • 约束 - 一个约束是一个用例运行的条件或限制。它包括:前置条件,后置条件和不变化条件 。前置条件指明了用例在发生之前需要符合的条件。后置条件用来说明在用例执行之后一些条件必须为”真”。不变化条件说明用例整个执行过程中该条件始终为”真”。
  • 情形 - 情形是用例的实例在执行过程中,事件发生流程的形式描述。它定义了系统和外部执行者之间的事件指定顺序。通常用文本方式来表示,并对应时序图中的文字描述。
  • 情形图
  • 附加信息

包含用例

用例可能包含其他用例的功能来作为它正常处理的一部分。通常它假设,任何被包含的用例在基本程序运行时每一次都会被调用。下面例子:用例“卡的确认”<Card Identification> 在运行时,被用例“取钱”<Withdraw>当作一个子部分。

用例可以被一个或多个用例包含。通过提炼通用的行为,将它变成可以多次重复使用的用例。有助于降低功能重复级别。

扩展用例

一个用例可以被用来扩展另一个用例的行为,通常使用在特别情况下。例如:假设在修改一个特别类型的客户订单之前,用户必须得到某种更高级别的许可,然后“获得许可”<Get Approval>用例将有选择的扩展常规的“修改订单”<Modify Order>用例。

扩展点 - 扩展用例的加入点被定义为扩展点。

系统边界 - 它用来显示用例在系统内部,执行者在系统的外部。

通信图

通信图,以前称之为协作图,是一种交互图,所显示消息与时序图相似,但是它更侧重于对象间的联系

在通信图中,对象之间显示关联连接器。消息附加到这些关联上,显示短箭头指向消息流的方向。消息的顺序通过编号码显示。

下面的两个图用通信图和时序图分别显示相同的信息。尽管我们可能从通信图的编号码得到消息顺序,但它不是立即可见的。通信图十分清楚的显示了邻近对象间全部完整的消息传递。

交互概述图

一个交互概览图是活动图的一种形式,它的节点代表交互图。交互图包含时序图,通信图,交互概览图和时间图。 大多数交互概览图标注与活动图一样。例如:起始,结束,判断,合并,分叉和结合节点是完全相同。并且,交互概览图介绍了两种新的元素:交互发生和交互元素。

交互发生

交互发生引用现有的交互图。显示为一个引用框,左上角显示 “ref” 。被引用的图名显示在框的中央。

交互元素

交互元素与交互发生相似之处在于都是在一个矩形框中显示一个现有的交互图。不同之处在内部显示参考图的内容不同。

将它们放在一起

所有的活动图控件,都可以相同地被使用于交互概览图,如:分叉,结合,合并等等。它把控制逻辑放入较低一级的图中。下面的例子就说明了一个典型的销售过程。子过程是从交互发生抽象而来。

时序图

时序图是交互图的一种形式,它显示对象沿生命线发展,对象之间随时间的交互表示为从源生命线指向目标生命线的消息。时序图能很好地显示那些对象与其它那些对象通信,什么消息触发了这些通信,时序图不能很好显示复杂过程的逻辑。

生命线

一条生命线在时序图中代表一个独立的参与者。表示为包含对象名的矩形,如果它的名字是”self”,则说明该生命线代表控制带时序图的类元。

有时,时序图会包含一个顶端是执行者的生命线。这情况说明掌握这个时序图的是用例。健壮图中的边界,控制和实体元素也可以有生命线。

消息

消息显示为箭头。消息可以完成传输,也可能丢失和找回,它可以是同步的,也可以是异步的,即可以是调用,也可以是信号。在下图中,第一条消息是同步消息(标为实箭头)完成传输,并隐含一条返回消息。第二条消息是异步消息 (标为实线箭头),第三条是异步返回消息(标为虚线)。

执行发生

向下延伸的细条状矩形表示执行事件或控制焦点的激活。在上图中有三个执行事件。第一个是源对象发送两条消息和收到两条回复。第二个是目标对象收到一条同步消息并返回一条回复。第三个是目标对象收到一条异步消息并返回一条回复。

内部通信

内部消息表现为一个操作的递归调用,或一个方法调用属于同一个对象的其他方法。显示为生命线上执行事件的嵌套控制焦点。

迷路消息和拾取消息

迷路消息是那些发送了却没有到达指定接收者,或者到达的接收者不再当前图中。拾取消息是收到来自那些未知的发送者,或者来自没有显示在当前图的发送者的消息。它们都表明是去往或来自一个终点元素。

生命线开始与结束

生命线可以在时序图时间刻度范围内创建和销毁,在下面的例子中,生命线被停止符号(叉号)终止。在前面的例子中,生命线顶端的符号(Child)显示在比创建它的对象符号(parent)沿页面要低的位置上。下图显示创建和终止对象。

时间和期限约束

消息默认显示为水平线。因为生命线显示为沿屏幕向下的时间通道,所以当给实时系统建模,或是有时间约束的业务过程建模,考虑执行动作所需时间长度是很重要的。因此可以给消息设置一个期限约束,这样的消息显示为下斜线。

复合片段

如前面所说,时序图不适合表达复杂的过程逻辑。在一种情况下,有许多机制允许把一定程度的过程逻辑加入到图中,并把它们放到复合片段的标题下。复合片段是一个或多个处理顺序被包含在一个框架中,并在指定名称的环境下执行。片段可以是:

  • 选择性片段 (显示 “alt”) 为 if…then…else 结构建模。
  • 选项片段 (显示 “opt”) 为 “switch”(开关) 结构建模。
  • 中断片段对被处理事件的可选择顺序建模,而不是该图的其他部分。
  • 并行片段(显示 “par”) 为并发处理建模。
  • 弱顺序片段 (显示 “seq”) 包含了一组消息,这组消息必须在后继片段开始之前被处理。但不会把片段内消息的先后顺序强加到不共享同一条生命线的消息上。
  • 严格顺序片段 (显示 “strict”) 包含了一系列需要按照给定顺序处理的消息。
  • 非片段 (显示 “neg”) 包含了一系列不可用的消息。
  • 关键片段 具有关键部分。
  • 忽略片段 声明一个没有意义的消息,如果它出现在当前上下文中。
  • 考虑片段与忽略片段相反,不包含在考虑片段内的消息都应该被忽略。
  • 断言片段 (显示 “assert”)标明任何没有显示为声明操作数的顺序都是无效的。
  • 循环片段 包含一系列被重复的消息。

下图显示的是循环片段:

这也是一个类似于复合片段的交互发生。 交互发生被其他图参考,显示为左上角带”ref”,将被参考图名显示在方框的中间。

门是连接片段内消息和片段外消息的连接点。 在 EA 中,门显示为片段框架上的小正方形。作用为时序图与页面外的连接器。 用来表示进来的消息源,或者出去消息的终点。下面两个图显示它们在实践中的使用。注意:” top level diagram”中的门用消息箭头指向参考片段,在这里没有必要把它画成方块。

部分分解

一个对象可以引出多条生命线,使得对象内部和对象之间的消息显示在同一图上。

状态常量/延续

状态常量是生命线的约束,运行时始终为”真”。显示为两侧半圆的矩形,如下图:

延续虽与状态常量有同样的标注,但是被用于复合片段,并可以延伸跨越多条生命线。

时间图

UML 时间图被用来显示随时间变化,一个或多个元素的值或状态的更改。也显示时控事件之间的交互和管理它们的时间和期限约束。

状态生命线

状态生命线显示随时间变化,一个单项状态的改变。不论时间单位如何选择,X 轴显示经过的时间,Y 轴被标为给出状态的列表。状态生命线如下所示:

值生命线

值生命线显示随时间变化,一个单项的值的变化。X 轴显示经过的时间,时间单位为任意,和状态生命线一样。平行线之间显示值,每次值变化,平行线交叉。如下图所示。

将它们放在一起

状态和值的生命线能叠加组合。它们必须有相同的 X 轴。 消息可以从一个生命线传递到另一个。每一个状态和值的变换能有一个定义的事件,一个时间限制是指一个事件何时必须发生,和一个期限限制说明状态或值多长时间必须有效。一旦这些已经被应用,其时间图可能显示如下。

参考资料

第一次读《重构:改善既有代码的设计》时,我曾整理过一个简单的笔记。最近,因为参与一个重构项目,再一次温习了《重构:改善既有代码的设计》。过程中,萌发了认真总结、整理重构方法的冲动,于是有了这系列文字。

症与药

对代码的坏味道的思考

“有病要早治,不要放弃治疗”。多么朴素的道理 ,人人都懂。

病,就是不健康。

人有病,可以通过打针、吃药、做手术来进行治疗。

如果把代码的坏味道(代码质量问题)比作病症,那么重构就是治疗代码的坏味道的药。

个人认为,在重构这件事上,也可以应用治病的道理:

  • 防患于未然。
    —— 春秋战国时期的一代名医扁鹊,曾经有个很著名的医学主张:防患于未然。 我觉得这个道理应用于软件代码的重构亦然。编程前要有合理的设计、编程时要有良好的编程风格,尽量减少问题。从这个层面上说,了解代码的坏味道,不仅仅是为了发现问题、解决问题。更重要的作用是:指导我们在编程过程中有意识的去规避这些问题。

  • 小病不医,易得大病。
    —— 刘备说过:“勿以善小而不为,勿以恶小而为之”。发现问题就及时修改,代码质量自然容易进入良性循环;反之,亦然。要重视积累的力量,别总以为代码出现点小问题,那都不是事儿。

  • 对症下药。
    —— 程序出现了问题,要分析出问题的根本,有针对性的制定合理的重构方案。大家都知道吃错药的后果,同样的,瞎改还不如不改

  • 忌猛药
    —— 医病用猛药容易产生副作用。换一句俗语:步子大了容易扯着蛋。重构如果大刀阔斧的干,那你就要有随时可能扑街的心理准备。推倒重来不是重构,而是重写。重构应该是循序渐进,步步为营的过程。当你发现重写代码比重构代码更简单,往往说明你早就该重构了。

重构的原则

前面把代码质量问题比作病症,而把重构比作药。这里,我们再进一步讨论一下重构的原则。

何谓重构(What)

重构(Refactoring) 的常见定义是:不改变软件系统外部行为的前提下,改善它的内部结构。

个人觉得这个定义有点生涩。不妨理解为:重构是给代码治病的行为。而代码有病是指代码的质量(可靠性、安全性、可复用性、可维护性)和性能有问题。

重构的目的是为了提高代码的质量和性能

注:功能不全或者不正确,那是残疾代码。就像治病治不了残疾,重构也解决不了功能问题。

为何重构(Why)

翻翻书,上网搜一下,谈到重构的理由大体相同:

  • 重构改进软件设计
  • 重构使软件更容易理解
  • 重构帮助找到 bug
  • 重构提高编程速度

总之就是,重构可以提高代码质量

何时重构(When)

关于何时重构,我先引用一下 重构并非难在如何做,而是难在何时开始做 一文的观点。

对于一个高速发展的公司来说,停止业务开发,专门来做重构项目,从来就不是一个可接受的选项,“边开飞机边换引擎”才是这种公司想要的。

我们不妨来衡量一下重构的成本和收益。

  • 重构的成本

    重构是有成本的,费时费力(时间、人力)不说,还有可能会使本来正常运行的程序出错。所以,很多人都抱着“不求有功,但求无过”的心理得过且过。

    还有一种成本:重构使用较新且较为复杂的技术,学习曲线不平滑,团队成员技术切换困难,短期内开发效率可能不升反降。

    但是,如果一直放任代码腐朽下去,技术债务会越来越沉重。当代码最终快要跑不动时,架构师们往往还是不得不使用激进的手段来治疗代码的顽疾。但是,这个过程通常都是非常痛苦的,而且有着很高的失败风险。

  • 重构的收益

    重构的收益是提高代码的质量和性能,并提高未来的开发效率。但是,应当看到,重构往往并不能在短期内带来实际的效益,或者很难直观看出效益。而对于一个企业来说,没有什么比效益更重要。换句话说,没有实际效益的事,通常也没有价值。很多领导,尤其是非技术方向的领导,并不关心你应用了什么新技术,让代码变得多么优雅等等。

  • 重构的合适时机

    从以上来看,重构实在是个吃力不讨好的事情。

    于是,很多人屈服于万恶的 KPI 和要命的 deadline,一边吐槽着以前的代码是垃圾,一边自己也在造垃圾。

    但是,重构本应该是个渐进式的过程,不是只有伤筋动骨的改造才叫重构。如果非要等到代码已经烂到病入膏肓,再使用激进方式来重构,那必然是困难重重,风险极高。

    《重构》书中提到的重构时机应该在添加功能、修复功能、审查代码时,不建议专门抽出时间专门做重构项目。

    我认为,其思想就是指:重构应该是在开发过程中实时的、渐进的演化过程。

  • 重构的不恰当时机

    但是,这里我也要强调一下:不是所有软件开发过程都一定要重构。

    较能凸显重构价值的场景是:代码规模较大、生命周期还较长、承担了较多责任、有一个较大(且较不稳定,人员流动频繁)团队在其上工作的单一代码库。

    与之相反,有一些场景的重构价值就很小:

    • 代码库生命周期快要走到尾声,开发逐渐减少,以维护为主。
    • 代码库当前版本马上要发布了,这时重构无疑是给自己找麻烦。
    • 重构代价过于沉重:重构后功能的正确性、稳定性难以保障;技术过于超前,团队成员技术迁移难度太大。

如何重构(How)

重构行为在我看来,也是可以分层级的。由高到低,越高层级难度越大:

  • 系统架构、集群架构、框架、服务、数据库:这个层面的重构属于战略级重构。现代软件往往业务复杂、庞大。使用微服务、数据迁移来拆分业务,降低业务复杂度成为了主流。但是,这些技术的测试、部署复杂,技术难度很高。
  • 组件、模块、接口:这个层面的重构属于战术级重构。组件、模块、框架的重构,主要是针对代码的设计问题。解决的是代码的整体结构问题。需要对框架、设计模式、分布式、并发等等有足够的了解。
  • 类、接口、函数、字段等:这个层面的重构属于战法级重构。《重构》一书提到了 代码的坏味道 以及相关的重构方法。这些都是对类、接口、函数、字段级别代码的重构手段。由于这一级别的重构方法较为简单,所以可操作性较强。具体细节可以阅读《代码的坏味道》篇章。

前两种层级的重构已经涉及到架构层面,影响较大,难度较高,如果功力不够不要轻易变动。由于这两个层级涉及领域较广,这里不做论述。

此处为分割线。下面是代码的坏味道系列。。。

代码的坏味道

《重构:改善既有代码的设计》中介绍了 22 种代码的坏味道以及重构手法。这些坏味道可以进一步归类。我总觉得将事物分类有助于理解和记忆。所以本系列将坏味道按照特性分类,然后逐一讲解。

img

代码坏味道之代码臃肿

代码臃肿(Bloated)这组坏味道意味着:代码中的类、函数、字段没有经过合理的组织,只是简单的堆砌起来。这一类型的问题通常在代码的初期并不明显,但是随着代码规模的增长而逐渐积累(特别是当没有人努力去根除它们时)。

代码坏味道之滥用面向对象

滥用面向对象(Object-Orientation Abusers)这组坏味道意味着:代码部分或完全地违背了面向对象编程原则。

代码坏味道之变革的障碍

变革的障碍(Change Preventers)这组坏味道意味着:当你需要改变一处代码时,却发现不得不改变其他的地方。这使得程序开发变得复杂、代价高昂。

代码坏味道之非必要的

非必要的(Dispensables)这组坏味道意味着:这样的代码可有可无,它的存在反而影响整体代码的整洁和可读性。

代码坏味道之耦合

耦合(Couplers)这组坏味道意味着:不同类之间过度耦合。

扩展阅读

参考资料