Dunwu Blog

大道至简,知易行难

Spring 集成调度器

概述

如果想在 Spring 中使用任务调度功能,除了集成调度框架 Quartz 这种方式,也可以使用 Spring 自己的调度任务框架。
使用 Spring 的调度框架,优点是:支持注解@Scheduler,可以省去大量的配置。

实时触发调度任务

TaskScheduler 接口

Spring3 引入了TaskScheduler接口,这个接口定义了调度任务的抽象方法。
TaskScheduler 接口的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface TaskScheduler {

ScheduledFuture schedule(Runnable task, Trigger trigger);

ScheduledFuture schedule(Runnable task, Date startTime);

ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period);

ScheduledFuture scheduleAtFixedRate(Runnable task, long period);

ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay);

ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay);

}

从以上方法可以看出 TaskScheduler 有两类重要参数:

  • 一个是要调度的方法,即一个实现了 Runnable 接口的线程类的 run()方法;
  • 另一个就是触发条件。

TaskScheduler 接口的实现类
它有三个实现类:DefaultManagedTaskSchedulerThreadPoolTaskSchedulerTimerManagerTaskScheduler
DefaultManagedTaskScheduler:基于 JNDI 的调度器。
TimerManagerTaskScheduler:托管commonj.timers.TimerManager实例的调度器。
ThreadPoolTaskScheduler:提供线程池管理的调度器,它也实现了TaskExecutor接口,从而使的单一的实例可以尽可能快地异步执行。

Trigger 接口

Trigger 接口抽象了触发条件的方法。
Trigger 接口的声明:

1
2
3
public interface Trigger {
Date nextExecutionTime(TriggerContext triggerContext);
}

Trigger 接口的实现类
CronTrigger:实现了 cron 规则的触发器类(和 Quartz 的 cron 规则相同)。
PeriodicTrigger:实现了一个周期性规则的触发器类(例如:定义触发起始时间、间隔时间等)。

完整范例

实现一个调度任务的功能有以下几个关键点:
(1) 定义调度器
在 spring-bean.xml 中进行配置
使用task:scheduler标签定义一个大小为 10 的线程池调度器,spring 会实例化一个ThreadPoolTaskScheduler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.1.xsd">
<mvc:annotation-driven/>
<task:scheduler id="myScheduler" pool-size="10"/>
</beans>

注:不要忘记引入 xsd:

1
2
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.1.xsd

(2) 定义调度任务
定义实现Runnable接口的线程类。

1
2
3
4
5
6
7
8
9
10
11
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DemoTask implements Runnable {
final Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public void run() {
logger.info("call DemoTask.run");
}
}

(3) 装配调度器,并执行调度任务
在一个Controller类中用@Autowired注解装配TaskScheduler
然后调动 TaskScheduler 对象的 schedule 方法启动调度器,就可以执行调度任务了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/scheduler")
public class SchedulerController {
@Autowired
TaskScheduler scheduler;

@RequestMapping(value = "/start", method = RequestMethod.POST)
public void start() {
scheduler.schedule(new DemoTask(), new CronTrigger("0/5 * * * * *"));
}
}

访问/scheduler/start 接口,启动调度器,可以看到如下日志内容:

1
2
3
4
13:53:15.010 myScheduler-1 o.zp.notes.spring.scheduler.DemoTask.run - call DemoTask.run
13:53:20.003 myScheduler-1 o.zp.notes.spring.scheduler.DemoTask.run - call DemoTask.run
13:53:25.004 myScheduler-2 o.zp.notes.spring.scheduler.DemoTask.run - call DemoTask.run
13:53:30.005 myScheduler-1 o.zp.notes.spring.scheduler.DemoTask.run - call DemoTask.run

@Scheduler 的使用方法

Spring 的调度器一个很大的亮点在于@Scheduler注解,这可以省去很多繁琐的配置。

启动注解

使用@Scheduler 注解先要使用<task:annotation-driven>启动注解开关。
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.1.xsd">
<mvc:annotation-driven/>
<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>
</beans>

@Scheduler 定义触发条件

例:使用fixedDelay指定触发条件为每 5000 毫秒执行一次。注意:必须在上一次调度成功后的 5000 秒才能执行。

1
2
3
4
@Scheduled(fixedDelay=5000)
public void doSomething() {
// something that should execute periodically
}

例:使用fixedRate指定触发条件为每 5000 毫秒执行一次。注意:无论上一次调度是否成功,5000 秒后必然执行。

1
2
3
4
@Scheduled(fixedRate=5000)
public void doSomething() {
// something that should execute periodically
}

例:使用initialDelay指定方法在初始化 1000 毫秒后才开始调度。

1
2
3
4
@Scheduled(initialDelay=1000, fixedRate=5000)
public void doSomething() {
// something that should execute periodically
}

例:使用cron表达式指定触发条件为每 5000 毫秒执行一次。cron 规则和 Quartz 中的 cron 规则一致。

1
2
3
4
@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
// something that should execute on weekdays only
}

完整范例

(1) 启动注解开关,并定义调度器和执行器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.1.xsd">

<mvc:annotation-driven/>
<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>
</beans>

(2) 使用@Scheduler 注解来修饰一个要调度的方法
下面的例子展示了@Scheduler 注解定义触发条件的不同方式。

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
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
* @description 使用@Scheduler注解调度任务范例
* @author Vicotr Zhang
* @date 2016年8月31日
*/
@Component
public class ScheduledMgr {
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

final Logger logger = LoggerFactory.getLogger(this.getClass());

/**
* 构造函数中打印初始化时间
*/
public ScheduledMgr() {
logger.info("Current time: {}", dateFormat.format(new Date()));
}

/**
* fixedDelay属性定义调度间隔时间。调度需要等待上一次调度执行完成。
*/
@Scheduled(fixedDelay = 5000)
public void testFixedDelay() throws Exception {
Thread.sleep(6000);
logger.info("Current time: {}", dateFormat.format(new Date()));
}

/**
* fixedRate属性定义调度间隔时间。调度不等待上一次调度执行完成。
*/
@Scheduled(fixedRate = 5000)
public void testFixedRate() throws Exception {
Thread.sleep(6000);
logger.info("Current time: {}", dateFormat.format(new Date()));
}

/**
* initialDelay属性定义初始化后的启动延迟时间
*/
@Scheduled(initialDelay = 1000, fixedRate = 5000)
public void testInitialDelay() throws Exception {
Thread.sleep(6000);
logger.info("Current time: {}", dateFormat.format(new Date()));
}

/**
* cron属性支持使用cron表达式定义触发条件
*/
@Scheduled(cron = "0/5 * * * * ?")
public void testCron() throws Exception {
Thread.sleep(6000);
logger.info("Current time: {}", dateFormat.format(new Date()));
}
}

我刻意设置触发方式的间隔都是 5s,且方法中均有 Thread.sleep(6000);语句。从而确保方法在下一次调度触发时间点前无法完成执行,来看一看各种方式的表现吧。
启动 spring 项目后,spring 会扫描@Component注解,然后初始化 ScheduledMgr。
接着,spring 会扫描@Scheduler注解,初始化调度器。调度器在触发条件匹配的情况下开始工作,输出日志。
截取部分打印日志来进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
10:58:46.479 localhost-startStop-1 o.z.n.s.scheduler.ScheduledTasks.<init> - Current time: 2016-08-31 10:58:46
10:58:52.523 myScheduler-1 o.z.n.s.scheduler.ScheduledTasks.testFixedRate - Current time: 2016-08-31 10:58:52
10:58:52.523 myScheduler-3 o.z.n.s.scheduler.ScheduledTasks.testFixedDelay - Current time: 2016-08-31 10:58:52
10:58:53.524 myScheduler-2 o.z.n.s.scheduler.ScheduledTasks.testInitialDelay - Current time: 2016-08-31 10:58:53
10:58:55.993 myScheduler-4 o.z.n.s.scheduler.ScheduledTasks.testCron - Current time: 2016-08-31 10:58:55
10:58:58.507 myScheduler-1 o.z.n.s.scheduler.ScheduledTasks.testFixedRate - Current time: 2016-08-31 10:58:58
10:58:59.525 myScheduler-5 o.z.n.s.scheduler.ScheduledTasks.testInitialDelay - Current time: 2016-08-31 10:58:59
10:59:03.536 myScheduler-3 o.z.n.s.scheduler.ScheduledTasks.testFixedDelay - Current time: 2016-08-31 10:59:03
10:59:04.527 myScheduler-1 o.z.n.s.scheduler.ScheduledTasks.testFixedRate - Current time: 2016-08-31 10:59:04
10:59:05.527 myScheduler-4 o.z.n.s.scheduler.ScheduledTasks.testInitialDelay - Current time: 2016-08-31 10:59:05
10:59:06.032 myScheduler-2 o.z.n.s.scheduler.ScheduledTasks.testCron - Current time: 2016-08-31 10:59:06
10:59:10.534 myScheduler-9 o.z.n.s.scheduler.ScheduledTasks.testFixedRate - Current time: 2016-08-31 10:59:10
10:59:11.527 myScheduler-10 o.z.n.s.scheduler.ScheduledTasks.testInitialDelay - Current time: 2016-08-31 10:59:11
10:59:14.524 myScheduler-4 o.z.n.s.scheduler.ScheduledTasks.testFixedDelay - Current time: 2016-08-31 10:59:14
10:59:15.987 myScheduler-6 o.z.n.s.scheduler.ScheduledTasks.testCron - Current time: 2016-08-31 10:59:15

构造方法打印一次,时间点在 10:58:46。
testFixedRate 打印四次,每次间隔 6 秒。说明,fixedRate 不等待上一次调度执行完成,在间隔时间达到时立即执行。
testFixedDelay 打印三次,每次间隔大于 6 秒,且时间不固定。说明,fixedDelay 等待上一次调度执行成功后,开始计算间隔时间,再执行。
testInitialDelay 第一次调度时间和构造方法调度时间相隔 7 秒。说明,initialDelay 在初始化后等待指定的延迟时间才开始调度。
testCron 打印三次,时间间隔并非 5 秒或 6 秒,显然,cron 等待上一次调度执行成功后,开始计算间隔时间,再执行。
此外,可以从日志中看出,打印日志的线程最多只有 10 个,说明 2.1 中的调度器线程池配置生效。

参考

Spring Framework 官方文档

Java 国际化

背景知识

通讯的发达,使得世界各地交流越来越紧密。许多的软件产品也要面向世界上不同国家的用户。其中,语言障碍显然是产品在不同语种用户中进行推广的一个重要问题。

本文围绕国际化这一主题,先介绍国际标准的语言编码,然后讲解在 Java 应用中如何去实现国际化。

语言编码、国家/地区编码

做 web 开发的朋友可能多多少少接触过类似 zh-cn, en-us 这样的编码字样。

这些编码是用来表示指定的国家地区的语言类型的。那么,这些含有特殊含义的编码是如何产生的呢?

ISO-639 标准使用编码定义了国际上常见的语言,每一种语言由两个小写字母表示。

ISO-3166 标准使用编码定义了国家/地区,每个国家/地区由两个大写字母表示。

下表列举了一些常见国家、地区的语言编码:

国家/地区 语言编码 国家/地区 语言编码
简体中文(中国) zh-cn 繁体中文(台湾地区) zh-tw
繁体中文(香港) zh-hk 英语(香港) en-hk
英语(美国) en-us 英语(英国) en-gb
英语(全球) en-ww 英语(加拿大) en-ca
英语(澳大利亚) en-au 英语(爱尔兰) en-ie
英语(芬兰) en-fi 芬兰语(芬兰) fi-fi
英语(丹麦) en-dk 丹麦语(丹麦) da-dk
英语(以色列) en-il 希伯来语(以色列) he-il
英语(南非) en-za 英语(印度) en-in
英语(挪威) en-no 英语(新加坡) en-sg
英语(新西兰) en-nz 英语(印度尼西亚) en-id
英语(菲律宾) en-ph 英语(泰国) en-th
英语(马来西亚) en-my 英语(阿拉伯) en-xa
韩文(韩国) ko-kr 日语(日本) ja-jp
荷兰语(荷兰) nl-nl 荷兰语(比利时) nl-be
葡萄牙语(葡萄牙) pt-pt 葡萄牙语(巴西) pt-br
法语(法国) fr-fr 法语(卢森堡) fr-lu
法语(瑞士) fr-ch 法语(比利时) fr-be
法语(加拿大) fr-ca 西班牙语(拉丁美洲) es-la
西班牙语(西班牙) es-es 西班牙语(阿根廷) es-ar
西班牙语(美国) es-us 西班牙语(墨西哥) es-mx
西班牙语(哥伦比亚) es-co 西班牙语(波多黎各) es-pr
德语(德国) de-de 德语(奥地利) de-at
德语(瑞士) de-ch 俄语(俄罗斯) ru-ru
意大利语(意大利) it-it 希腊语(希腊) el-gr
挪威语(挪威) no-no 匈牙利语(匈牙利) hu-hu
土耳其语(土耳其) tr-tr 捷克语(捷克共和国) cs-cz
斯洛文尼亚语 sl-sl 波兰语(波兰) pl-pl
瑞典语(瑞典) sv-se

注:由表中可以看出语言、国家/地区编码一般都是英文单词的缩写。

字符编码

在此处,引申一下字符编码的概念。

是不是有了语言、国家/地区编码,计算机就可以识别各种语言了?

答案是否。作为程序员,相信每个人都会遇到过这样的情况:期望打印中文,结果输出的却是乱码。

这种情况,往往是因为字符编码的问题。

计算机在设计之初,并没有考虑多个国家,多种不同语言的应用场景。当时定义一种ASCII码,将字母、数字和其他符号编号用 7 比特的二进制数来表示。后来,计算机在世界开始普及,为了适应多种文字,出现了多种编码格式,例如中文汉字一般使用的编码格式为GB2312GBK

由此,又产生了一个问题,不同字符编码之间互相无法识别。于是,为了一统江湖,出现了 unicode 编码。它为每种语言的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台的文本转换需求。

有人不禁要问,既然 Unicode 可以支持所有语言的字符,那还要其他字符编码做什么?

Unicode 有一个缺点:为了支持所有语言的字符,所以它需要用更多位数去表示,比如 ASCII 表示一个英文字符只需要一个字节,而 Unicode 则需要两个字节。很明显,如果字符数多,这样的效率会很低。

为了解决这个问题,有出现了一些中间格式的字符编码:如 UTF-8UTF-16UTF-32 等(中国的程序员一般使用UTF-8编码)。

Java 中实现国际化

国际化的实现原理很简单:

  1. 先定义好不同语种的模板;
  2. 选择语种;
  3. 加载指定语种的模板。

接下来,本文会按照步骤逐一讲解实现国际化的具体步骤

定义不同语种的模板

Java 中将多语言文本存储在格式为 properties 的资源文件中。

它必须遵照以下的命名规范:

1
<资源名>_<语言代码>_<国家/地区编码>.properties

其中,语言编码和国家/地区编码都是可选的。

注:<资源名>.properties 命名的国际化资源文件是默认的资源文件,即某个国际化类型在系统中找不到对应的资源文件,就采用这个默认的资源文件。

定义 properties 文件

src/main/resources/locales 路径下定义名为 content 的不同语种资源文件:

content_en_US.properties

1
2
helloWorld = HelloWorld!
time = The current time is %s.

content_zh_CN.properties

1
2
helloWorld = \u4e16\u754c\uff0c\u4f60\u597d\uff01
time = \u5f53\u524d\u65f6\u95f4\u662f\u0025\u0073\u3002

可以看到:几个资源文件中,定义的 Key 完全一致,只是 Value 是对应语言的字符串。

虽然属性值各不相同,但属性名却是相同的,这样应用程序就可以通过 Locale 对象和属性名精确调用到某个具体的属性值了。

Unicode 转换工具

上一节中,我们定义的中文资源文件中的属性值都是以\u 开头的四位 16 进制数。其实,这表示的是一个 Unicode 编码。

1
2
helloWorld = \u4e16\u754c\uff0c\u4f60\u597d\uff01
time = \u5f53\u524d\u65f6\u95f4\u662f\u0025\u0073\u3002

本文的字符编码中提到了,为了达到跨编码也正常显示的目的,有必要将非 ASCII 字符转为 Unicode 编码。上面的中文资源文件就是中文转为 Unicode 的结果。

怎么将非 ASCII 字符转为 Unicode 编码呢?

JDK 在 bin 目录下为我们提供了一个转换工具:native2ascii

它可以将中文字符的资源文件转换为 Unicode 代码格式的文件,命令格式如下:

1
native2ascii [-reverse] [-encoding 编码] [输入文件 [输出文件]]

假设content_zh_CN.propertiesd:\ 目录。执行以下命令可以新建一个名为 content_zh_CN_new.properties 的文件,其中的内容就中文字符转为 UTF-8 编码格式的结果。

1
native2ascii -encoding utf-8 d:\content_zh_CN.properties d:\content_zh_CN_new.properties

选择语种

定义了多语言资源文件,第二步就是根据本地语种选择模板文件了。

Locale

在 Java 中,一个 java.util.Locale 对象表示了特定的地理、政治和文化地区。需要 Locale 来执行其任务的操作称为语言环境敏感的操作,它使用 Locale 为用户量身定制本地信息。

它有三个构造方法

Locale(String language) :根据语言编码初始化
Locale(String language, String country) :根据语言编码、国家编码初始化
Locale(String language, String country, String variant) :根据语言编码、国家编码、变体初始化

此外,Locale 定义了一些常用的 Locale 常量:Locale.ENGLISHLocale.CHINESE 等。

1
2
3
4
5
6
7
8
// 初始化一个通用英语的locale.
Locale locale1 = new Locale("en");
// 初始化一个加拿大英语的locale.
Locale locale2 = new Locale("en", "CA");
// 初始化一个美式英语变种硅谷英语的locale
Locale locale3 = new Locale("en", "US", "SiliconValley");
// 根据Locale常量初始化一个简体中文
Locale locale4 = Locale.SIMPLIFIED_CHINESE;

加载指定语种的模板

ResourceBoundle

Java 为我们提供了用于加载国际化资源文件的工具类:java.util.ResourceBoundle

ResourceBoundle 提供了多个名为 getBundle 的静态重载方法,这些方法的作用是用来根据资源名、Locale 选择指定语种的资源文件。需要说明的是: getBundle 方法的第一个参数一般都是baseName ,这个参数表示资源文件名。

ResourceBoundle 还提供了名为 getString 的方法,用来获取资源文件中 key 对应的 value。

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
import java.util.Locale;
import java.util.ResourceBundle;

public class ResourceBundleDemo {

public static void main(String[] args) {
// 根据语言+地区编码初始化
ResourceBundle rbUS = ResourceBundle.getBundle("locales.content", new Locale("en", "US"));
// 根据Locale常量初始化
ResourceBundle rbZhCN = ResourceBundle.getBundle("locales.content", Locale.SIMPLIFIED_CHINESE);
// 获取本地系统默认的Locale初始化
ResourceBundle rbDefault = ResourceBundle.getBundle("locales.content");
// ResourceBundle rbDefault =ResourceBundle.getBundle("locales.content", Locale.getDefault()); // 与上行代码等价

System.out.println("en-US:" + rbUS.getString("helloWorld"));
System.out.println("en-US:" + String.format(rbUS.getString("time"), "08:00"));
System.out.println("zh-CN:" + rbZhCN.getString("helloWorld"));
System.out.println("zh-CN:" + String.format(rbZhCN.getString("time"), "08:00"));
System.out.println("default:" + rbDefault.getString("helloWorld"));
System.out.println("default:" + String.format(rbDefault.getString("time"), "08:00"));
}

}

// 输出:
// en-US:HelloWorld!
// en-US:The current time is 08:00.
// zh-CN:世界,你好!
// zh-CN:当前时间是08:00。
// default:世界,你好!
// default:当前时间是08:00。

注:在加载资源时,如果指定的国际化资源文件不存在,它会尝试按下面的顺序加载其他的资源:本地系统默认国际化对象对应的资源 -> 默认的资源。如果指定错误,Java 会提示找不到资源文件。

支持国际化的工具类

Java 中也提供了几个支持国际化的格式化工具类。例如:NumberFormatDateFormatMessageFormat

NumberFormat

NumberFormat 是所有数字格式类的基类。它提供格式化和解析数字的接口。它也提供了决定数字所属语言类型的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.text.NumberFormat;
import java.util.Locale;

public class NumberFormatDemo {

public static void main(String[] args) {
double num = 123456.78;
NumberFormat format = NumberFormat.getCurrencyInstance(Locale.SIMPLIFIED_CHINESE);
System.out.format("%f 的国际化(%s)结果: %s\n", num, Locale.SIMPLIFIED_CHINESE, format.format(num));
}

}

// 输出:
// 123456.780000 的国际化(zh_CN)结果: ¥123,456.78

DateFormat

DateFormat 是日期、时间格式化类的抽象类。它支持基于语言习惯的日期、时间格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

public class DateFormatDemo {

public static void main(String[] args) {
Date date = new Date();
DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.ENGLISH);
DateFormat df2 = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.SIMPLIFIED_CHINESE);
System.out.format("%s 的国际化(%s)结果: %s\n", date, Locale.ENGLISH, df.format(date));
System.out.format("%s 的国际化(%s)结果: %s\n", date, Locale.SIMPLIFIED_CHINESE, df2.format(date));
}

}

// 输出
// Fri Dec 23 11:14:45 CST 2022 的国际化(en)结果: Dec 23, 2022
// Fri Dec 23 11:14:45 CST 2022 的国际化(zh_CN)结果: 2022-12-23

MessageFormat

Messageformat 提供一种与语言无关的拼接消息的方式。通过这种拼接方式,将最终呈现返回给使用者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.text.MessageFormat;
import java.util.GregorianCalendar;
import java.util.Locale;

public class MessageFormatDemo {

public static void main(String[] args) {
String pattern1 = "{0},你好!你于 {1} 消费 {2} 元。";
String pattern2 = "At {1,time,short} On {1,date,long},{0} paid {2,number, currency}.";
Object[] params = { "Jack", new GregorianCalendar().getTime(), 8888 };
String msg1 = MessageFormat.format(pattern1, params);
MessageFormat mf = new MessageFormat(pattern2, Locale.US);
String msg2 = mf.format(params);
System.out.println(msg1);
System.out.println(msg2);
}

}

// 输出:
// Jack,你好!你于 22-12-23 上午11:05 消费 8,888 元。
// At 11:05 AM On December 23, 2022,Jack paid $8,888.00.

Spring 集成 Dubbo

ZooKeeper

ZooKeeper 可以作为 Dubbo 的注册中心。

Dubbo 未对 Zookeeper 服务器端做任何侵入修改,只需安装原生的 Zookeeper 服务器即可,所有注册中心逻辑适配都在调用 Zookeeper 客户端时完成。

安装

ZooKeeper 发布中心 选择需要的版本,下载后解压到本地。

配置

1
2
vi conf/zoo.cfg

如果不需要集群,zoo.cfg 的内容如下 2

1
2
3
4
5
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/home/dubbo/zookeeper-3.3.3/data
clientPort=2181

如果需要集群,zoo.cfg 的内容如下 3

1
2
3
4
5
6
7
8
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/home/dubbo/zookeeper-3.3.3/data
clientPort=2181
server.1=10.20.153.10:2555:3555
server.2=10.20.153.11:2555:3555

并在 data 目录 4 下放置 myid 文件:

1
2
3
mkdir data
vi myid

myid 指明自己的 id,对应上面 zoo.cfgserver. 后的数字,第一台的内容为 1,第二台的内容为 2,内容如下:

1
2
1

启动

Linux 下执行 bin/zkServer.sh ;Windows bin/zkServer.cmd 启动 ZooKeeper 。

命令行

1
2
telnet 127.0.0.1 2181
dump

或者:

1
echo dump | nc 127.0.0.1 2181

用法:

1
2
dubbo.registry.address=zookeeper://10.20.153.10:2181?backup=10.20.153.11:2181

或者:

1
2
<dubbo:registry protocol="zookeeper" address="10.20.153.10:2181,10.20.153.11:2181" />

  1. Zookeeper 是 Apache Hadoop 的子项目,强度相对较好,建议生产环境使用该注册中心
  2. 其中 data 目录需改成你真实输出目录
  3. 其中 data 目录和 server 地址需改成你真实部署机器的信息
  4. 上面 zoo.cfg 中的 dataDir
  5. http://zookeeper.apache.org/doc/r3.3.3/zookeeperAdmin.html

Dubbo

Dubbo 采用全 Spring 配置方式,透明化接入应用,对应用没有任何 API 侵入,只需用 Spring 加载 Dubbo 的配置即可,Dubbo 基于 Spring 的 Schema 扩展进行加载。

如果不想使用 Spring 配置,可以通过 API 的方式 进行调用。

服务提供者

完整安装步骤,请参见:示例提供者安装

定义服务接口

DemoService.java 1

1
2
3
4
5
package com.alibaba.dubbo.demo;

public interface DemoService {
String sayHello(String name);
}

在服务提供方实现接口

DemoServiceImpl.java 2

1
2
3
4
5
6
7
8
9
package com.alibaba.dubbo.demo.provider;

import com.alibaba.dubbo.demo.DemoService;

public class DemoServiceImpl implements DemoService {
public String sayHello(String name) {
return "Hello " + name;
}
}

用 Spring 配置声明暴露服务

provider.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="hello-world-app" />

<!-- 使用multicast广播注册中心暴露服务地址 -->
<dubbo:registry address="multicast://224.5.6.7:1234" />

<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" />

<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" />

<!-- 和本地bean一样实现服务 -->
<bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl" />
</beans>

如果注册中心使用 ZooKeeper,可以将 dubbo:registry 改为 zookeeper://127.0.0.1:2181

加载 Spring 配置

Provider.java:

1
2
3
4
5
6
7
8
9
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Provider {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"http://10.20.160.198/wiki/display/dubbo/provider.xml"});
context.start();
System.in.read(); // 按任意键退出
}
}

服务消费者

完整安装步骤,请参见:示例消费者安装

通过 Spring 配置引用远程服务

consumer.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
<dubbo:application name="consumer-of-helloworld-app" />

<!-- 使用multicast广播注册中心暴露发现服务地址 -->
<dubbo:registry address="multicast://224.5.6.7:1234" />

<!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
<dubbo:reference id="demoService" interface="com.alibaba.dubbo.demo.DemoService" />
</beans>

如果注册中心使用 ZooKeeper,可以将 dubbo:registry 改为 zookeeper://127.0.0.1:2181

加载 Spring 配置,并调用远程服务

Consumer.java 3

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.alibaba.dubbo.demo.DemoService;

public class Consumer {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"http://10.20.160.198/wiki/display/dubbo/consumer.xml"});
context.start();
DemoService demoService = (DemoService)context.getBean("demoService"); // 获取远程服务代理
String hello = demoService.sayHello("world"); // 执行远程方法
System.out.println( hello ); // 显示调用结果
}
}
  1. 该接口需单独打包,在服务提供方和消费方共享
  2. 对服务消费方隐藏实现
  3. 也可以使用 IoC 注入

FAQ

建议使用 dubbo-2.3.3 以上版本的 zookeeper 注册中心客户端。

资料

Dubbo

Github | 用户手册 | 开发手册 | 管理员手册

ZooKeeper

官网 | 官方文档

Spring 之数据源

本文基于 Spring Boot 2.7.3 版本。

Spring Boot 数据源基本配置

Spring Boot 提供了一系列 spring.datasource.* 配置来控制 DataSource 的配置。用户可以在 application.propertiesapplication.yml 文件中指定数据源配置。这些配置项维护在 DataSourceProperties

下面是一个最基本的 mysql 数据源配置示例(都是必填项):

1
2
3
4
5
6
7
8
# 数据库访问地址
spring.datasource.url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8
# 数据库驱动类,必须保证驱动类是可加载的
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
# 数据库账号
spring.datasource.username = root
# 数据库账号密码
spring.datasource.password = root

需要根据实际情况,替换 urlusernamepassword

Spring Boot 连接嵌入式数据源

使用内存嵌入式数据库开发应用程序通常很方便。显然,内存数据库不提供持久存储。使用者需要在应用程序启动时填充数据库,并准备在应用程序结束时丢弃数据。

Spring Boot 可以自动配置嵌入式数据库 H2HSQLDerby。使用者无需提供任何连接 URL,只需要包含对要使用的嵌入式数据库的构建依赖项。如果类路径上有多个嵌入式数据库,需要设置 spring.datasource.embedded-database-connection 配置属性来控制使用哪一个。将该属性设置为 none 会禁用嵌入式数据库的自动配置。

注意:如果在测试中使用此功能,无论使用多少应用程序上下文,整个测试套件都会重用同一个数据库。如果要确保每个上下文都有一个单独的嵌入式数据库,则应将 spring.datasource.generate-unique-name 设置为 true。

下面,通过一个实例展示如何连接 H2 嵌入式数据库。

(1)在 pom.xml 中引入所需要的依赖:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>

(2)数据源配置

1
2
3
4
spring.datasource.jdbc-url = jdbc:h2:mem:test
spring.datasource.driver-class-name = org.h2.Driver
spring.datasource.username = sa
spring.datasource.password =

Spring Boot 连接池化数据源

完整示例:spring-boot-data-jdbc

在生产环境中,出于性能考虑,一般会通过数据库连接池连接数据源。

除了 DataSourceProperties 中的数据源通用配置以外,Spring Boot 还支持通过使用类似spring.datasource.hikari.*spring.datasource.tomcat.*spring.datasource.dbcp2.*spring.datasource.oracleucp.* 的前缀来配置指定的数据库连接池属性。

下面,就是一份 hikari 的连接池配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 连接池名称
spring.datasource.hikari.pool-name = SpringTutorialHikariPool
# 最大连接数,小于等于 0 会被重置为默认值 10;大于零小于 1 会被重置为 minimum-idle 的值
spring.datasource.hikari.maximum-pool-size = 10
# 最小空闲连接,默认值10,小于 0 或大于 maximum-pool-size,都会重置为 maximum-pool-size
spring.datasource.hikari.minimum-idle = 10
# 连接超时时间(单位:毫秒),小于 250 毫秒,会被重置为默认值 30 秒
spring.datasource.hikari.connection-timeout = 60000
# 空闲连接超时时间,默认值 600000(10分钟),大于等于 max-lifetime 且 max-lifetime>0,会被重置为0;不等于 0 且小于 10 秒,会被重置为 10 秒
# 只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放
spring.datasource.hikari.idle-timeout = 600000
# 连接最大存活时间,不等于 0 且小于 30 秒,会被重置为默认值 30 分钟。该值应该比数据库所设置的超时时间短
spring.datasource.hikari.max-lifetime = 540000

Spring Boot 会按以下顺序检测连接池是否可用,如果可用就选择对应的池化 DataSource

HikariCP -> Tomcat pooling DataSource -> DBCP2 -> Oracle UCP

用户也可以通过 spring.datasource.type 来指定数据源类型。

此外,也可以使用 DataSourceBuilder 手动配置其他连接池。如果自定义 DataSource bean,则不会发生自动配置。 DataSourceBuilder 支持以下连接池:

  • HikariCP
  • Tomcat pooling Datasource
  • Commons DBCP2
  • Oracle UCP & OracleDataSource
  • Spring Framework’s SimpleDriverDataSource
  • H2 JdbcDataSource
  • PostgreSQL PGSimpleDataSource
  • C3P0

引入 Spring Boot 依赖

你可以通过 Spring Boot 官方的初始化器(Spring Initializr)选择需要的组件来创建一个 Spring Boot 工程。或者,直接在 pom.xml 中引入所需要的依赖:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

测试单数据源连接

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
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jdbc.core.JdbcTemplate;

import java.sql.Connection;
import javax.sql.DataSource;

@Slf4j
@SpringBootApplication
public class SpringBootDataJdbcApplication implements CommandLineRunner {

private final JdbcTemplate jdbcTemplate;

public SpringBootDataJdbcApplication(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

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

@Override
public void run(String... args) throws Exception {
DataSource dataSource = jdbcTemplate.getDataSource();

Connection connection;
if (dataSource != null) {
connection = dataSource.getConnection();
} else {
log.error("连接数据源失败!");
return;
}

if (connection != null) {
log.info("数据源 Url: {}", connection.getMetaData().getURL());
} else {
log.error("连接数据源失败!");
}
}

}

运行 main 方法后,控制台会输出以下内容,表示数据源连接成功:

1
20:50:18.449 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcApplication.run - 数据源 Url: jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8

Spring Boot 连接多数据源

完整示例:spring-boot-data-jdbc-multi-datasource

Spring Boot 连接多数据源所需要的依赖并无不同,主要差异在于数据源的配置。Spring Boot 默认的数据源配置类为 org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration。使用者只要指定一些必要的 spring.datasource 配置,DataSourceAutoConfiguration 类就会自动完成剩下的数据源实例化工作。

多数据源配置

下面的示例中,自定义了一个数据源配置类,通过读取不同的 spring.datasource.xxx 来完成对于不同数据源的实例化工作。对于 JDBC 来说,最重要的,就是实例化 DataSourceJdbcTemplate

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
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

@Configuration
public class DataSourceConfig {

@Primary
@Bean("mysqlDataSource")
@ConfigurationProperties(prefix = "spring.datasource.mysql")
public DataSource mysqlDataSource() {
return DataSourceBuilder.create().build();
}

@Primary
@Bean("mysqlJdbcTemplate")
public JdbcTemplate mysqlJdbcTemplate(@Qualifier("mysqlDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}

@Bean("h2DataSource")
@ConfigurationProperties(prefix = "spring.datasource.h2")
public DataSource h2DataSource() {
return DataSourceBuilder.create().build();
}

@Bean(name = "h2JdbcTemplate")
public JdbcTemplate h2JdbcTemplate(@Qualifier("h2DataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}

}

application.propertiesapplication.yml 配置文件中也必须以 @ConfigurationProperties 所指定的配置前缀进行配置:

1
2
3
4
5
6
7
8
9
10
# 数据源一:Mysql
spring.datasource.mysql.jdbc-url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.mysql.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.mysql.username = root
spring.datasource.mysql.password = root
# 数据源一:H2
spring.datasource.h2.jdbc-url = jdbc:h2:mem:test
spring.datasource.h2.driver-class-name = org.h2.Driver
spring.datasource.h2.username = sa
spring.datasource.h2.password =

测试多数据源连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jdbc.core.JdbcTemplate;

import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;

@SpringBootApplication
public class SpringBootDataJdbcMultiDataSourceApplication implements CommandLineRunner {

private static final Logger log = LoggerFactory.getLogger(SpringBootDataJdbcMultiDataSourceApplication.class);

private final UserDao mysqlUserDao;

private final UserDao h2UserDao;

public SpringBootDataJdbcMultiDataSourceApplication(@Qualifier("mysqlUserDao") UserDao mysqlUserDao,
@Qualifier("h2UserDao") UserDao h2UserDao) {
this.mysqlUserDao = mysqlUserDao;
this.h2UserDao = h2UserDao;
}

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

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

if (mysqlUserDao != null && mysqlUserDao.getJdbcTemplate() != null) {
printDataSourceInfo(mysqlUserDao.getJdbcTemplate());
log.info("Connect to mysql datasource success.");
} else {
log.error("Connect to mysql datasource failed!");
return;
}

if (h2UserDao != null) {
printDataSourceInfo(h2UserDao.getJdbcTemplate());
log.info("Connect to h2 datasource success.");
} else {
log.error("Connect to h2 datasource failed!");
return;
}

// 主数据源执行 JDBC SQL
mysqlUserDao.recreateTable();

// 次数据源执行 JDBC SQL
h2UserDao.recreateTable();
}

private void printDataSourceInfo(JdbcTemplate jdbcTemplate) throws SQLException {

DataSource dataSource = jdbcTemplate.getDataSource();

Connection connection;
if (dataSource != null) {
connection = dataSource.getConnection();
} else {
log.error("Get dataSource failed!");
return;
}

if (connection != null) {
log.info("DataSource Url: {}", connection.getMetaData().getURL());
} else {
log.error("Connect to datasource failed!");
}
}

}

运行 main 方法后,控制台会输出以下内容,表示数据源连接成功:

1
2
3
4
5
21:16:44.654 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.printDataSourceInfo - DataSource Url: jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
21:16:44.654 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.run - Connect to mysql datasource success.

21:16:44.726 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.printDataSourceInfo - DataSource Url: jdbc:h2:mem:test
21:16:44.726 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.run - Connect to h2 datasource success.

Spring 之数据源

如果你的项目是传统的 Spring 项目,当然也可以轻松建立数据源连接,只是需要自行设置的配置更多一些。

引入 Spring 依赖

在 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
    <dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
</dependencies>
</project>

Spring 配置数据源

Spring 配置数据源有多种方式,下面一一列举:

使用 JNDI 数据源

如果 Spring 应用部署在支持 JNDI 的 WEB 服务器上(如 WebSphere、JBoss、Tomcat 等),就可以使用 JNDI 获取数据源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee-3.2.xsd">

<!-- 1.使用bean配置jndi数据源 -->
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="java:comp/env/jdbc/orclight" />
</bean>

<!-- 2.使用jee标签配置jndi数据源,与1等价,但是需要引入命名空间 -->
<jee:jndi-lookup id="dataSource" jndi-name=" java:comp/env/jdbc/orclight" />
</beans>

使用数据库连接池

Spring 本身并没有提供数据库连接池的实现,需要自行选择合适的数据库连接池。下面是一个使用 Druid 作为数据库连接池的示例:

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
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>

<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="1"/>
<property name="minIdle" value="1"/>
<property name="maxActive" value="10"/>

<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="10000"/>

<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/>

<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000"/>

<property name="testWhileIdle" value="true"/>

<!-- 这里建议配置为TRUE,防止取到的连接不可用 -->
<property name="testOnBorrow" value="true"/>
<property name="testOnReturn" value="false"/>

<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="true"/>
<property name="maxPoolPreparedStatementPerConnectionSize"
value="20"/>

<!-- 这里配置提交方式,默认就是TRUE,可以不用配置 -->

<property name="defaultAutoCommit" value="true"/>

<!-- 验证连接有效与否的SQL,不同的数据配置不同 -->
<property name="validationQuery" value="select 1 "/>
<property name="filters" value="stat"/>
</bean>

基于 JDBC 驱动的数据源

1
2
3
4
5
6
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>

SpringBoot 数据源配置

Spring Boot 数据库配置官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.sql

通过前面的实战,我们已经知道了 Spring、Spring Boot 是如何连接数据源,并通过 JDBC 方式访问数据库。

SpringBoot 数据源的配置方式是在 application.propertiesapplication.yml 文件中指定 spring.datasource.* 的配置。

(1)数据源基本配置方式是指定 url、用户名、密码

1
2
3
spring.datasource.url=jdbc:mysql://localhost/test
spring.datasource.username=dbuser
spring.datasource.password=dbpass

(2)配置 JNDI

如果想要通过 JNDI 方式连接数据源,可以采用如下方式:

1
spring.datasource.jndi-name=java:jboss/datasources/customers

DataSourceAutoConfiguration 类

显而易见,Spring Boot 的配置更加简化,那么, Spring Boot 做了哪些工作,使得接入更加便捷呢?奥秘就在于 spring-boot-autoconfigure jar 包,其中定义了大量的 Spring Boot 自动配置类。其中,与数据库访问相关的比较核心的配置类有:

  • DataSourceAutoConfiguration:数据源自动配置类
  • JdbcTemplateAutoConfigurationJdbcTemplate 自动配置类
  • DataSourceTransactionManagerAutoConfiguration:数据源事务管理自动配置类
  • JndiDataSourceAutoConfiguration:JNDI 数据源自动配置类
  • EmbeddedDataSourceConfiguration:嵌入式数据库数据源自动配置类
  • 等等

这些自动配置类会根据各种条件控制核心类的实例化。

DataSourceAutoConfiguration 是数据源自动配置类,它负责实例化 DataSource

DataSourceAutoConfiguration 的源码如下(省略部分代码):

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
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import(DataSourcePoolMetadataProvidersConfiguration.class)
public class DataSourceAutoConfiguration {

@Configuration(proxyBeanMethods = false)
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {
}

@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}

static class PooledDataSourceCondition extends AnyNestedCondition {
// 略
}

static class PooledDataSourceAvailableCondition extends SpringBootCondition {
// 略
}

static class EmbeddedDatabaseCondition extends SpringBootCondition {
// 略
}
}

DataSourceAutoConfiguration 类的源码解读:

  • DataSourcePropertiesDataSourceAutoConfiguration 的配置选项类,允许使用者通过设置选项控制 DataSource 初始化行为。
  • DataSourceAutoConfiguration 通过 @Import 注解引入 DataSourcePoolMetadataProvidersConfiguration 类。
  • DataSourceAutoConfiguration 中定义了两个内部类:嵌入式数据源配置类 EmbeddedDatabaseConfiguration 和 池化数据源配置类 PooledDataSourceConfiguration,分别标记了不同的实例化条件。
    • 当满足 EmbeddedDatabaseConfiguration 的示例化条件时,将引入 EmbeddedDataSourceConfiguration 类初始化数据源,这个类实际上是加载嵌入式数据源驱动的 ClassLoader 去进行初始化。
    • 当满足 PooledDataSourceConfiguration 的示例化条件时,将引入 DataSourceConfiguration.Hikari.classDataSourceConfiguration.Tomcat.classDataSourceConfiguration.Dbcp2.classDataSourceConfiguration.OracleUcp.classDataSourceConfiguration.Generic.classDataSourceJmxConfiguration.class 这些配置类,分别对应不同的数据库连接池方式。具体选用哪种数据库连接池,可以通过 spring.datasource.type 配置指定。其中,Hikari 是 Spring Boot 默认的数据库连接池,spring-boot-starter-data-jdbc 中内置了 Hikari 连接池驱动包。如果想要替换其他数据库连接池,前提是必须先手动引入对应的连接池驱动包。

参考资料

正则表达式极简教程

简介

为了理解下面章节的内容,你需要先了解一些基本概念。

  • 正则表达式 - 正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
  • 元字符 - 元字符(metacharacters)就是正则表达式中具有特殊意义的专用字符。
  • 普通字符 - 普通字符包括没有显式指定为元字符的所有可打印和不可打印字符。这包括所有大写和小写字母、所有数字、所有标点符号和一些其他符号。

基本元字符

正则表达式的元字符难以记忆,很大程度上是因为有很多为了简化表达而出现的等价字符。

而实际上最基本的元字符,并没有那么多。对于大部分的场景,基本元字符都可以搞定。

让我们从一个个实例出发,由浅入深的去体会正则的奥妙。

多选 - |

例 匹配一个确定的字符串

1
checkMatches("abc", "abc");

如果要匹配一个确定的字符串,非常简单,如例 1 所示。

如果你不确定要匹配的字符串,希望有多个选择,怎么办?

答案是:使用元字符| ,它的含义是或。

例 匹配多个可选的字符串

1
2
3
4
// 测试正则表达式字符:|
Assert.assertTrue(checkMatches("yes|no", "yes"));
Assert.assertTrue(checkMatches("yes|no", "no"));
Assert.assertFalse(checkMatches("yes|no", "right"));

输出

1
2
3
yes	matches: yes|no
no matches: yes|no
right not matches: yes|no

分组 - ()

如果你希望表达式由多个子表达式组成,你可以使用 ()

例 匹配组合字符串

1
2
3
4
Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "ended"));
Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "ending"));
Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "playing"));
Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "played"));

输出

1
2
3
4
ended	matches: (play|end)(ing|ed)
ending matches: (play|end)(ing|ed)
playing matches: (play|end)(ing|ed)
played matches: (play|end)(ing|ed)

指定单字符有效范围 - []

前面展示了如何匹配字符串,但是很多时候你需要精确的匹配一个字符,这时可以使用[]

例 字符在指定范围

1
2
3
4
5
6
7
// 测试正则表达式字符:[]
Assert.assertTrue(checkMatches("[abc]", "b")); // 字符只能是a、b、c
Assert.assertTrue(checkMatches("[a-z]", "m")); // 字符只能是a - z
Assert.assertTrue(checkMatches("[A-Z]", "O")); // 字符只能是A - Z
Assert.assertTrue(checkMatches("[a-zA-Z]", "K")); // 字符只能是a - z和A - Z
Assert.assertTrue(checkMatches("[a-zA-Z]", "k"));
Assert.assertTrue(checkMatches("[0-9]", "5")); // 字符只能是0 - 9

输出

1
2
3
4
5
6
b	matches: [abc]
m matches: [a-z]
O matches: [A-Z]
K matches: [a-zA-Z]
k matches: [a-zA-Z]
5 matches: [0-9]

指定单字符无效范围 - [^]

例 字符不能在指定范围

如果需要匹配一个字符的逆操作,即字符不能在指定范围,可以使用[^]

1
2
3
4
5
6
7
// 测试正则表达式字符:[^]
Assert.assertFalse(checkMatches("[^abc]", "b")); // 字符不能是a、b、c
Assert.assertFalse(checkMatches("[^a-z]", "m")); // 字符不能是a - z
Assert.assertFalse(checkMatches("[^A-Z]", "O")); // 字符不能是A - Z
Assert.assertFalse(checkMatches("[^a-zA-Z]", "K")); // 字符不能是a - z和A - Z
Assert.assertFalse(checkMatches("[^a-zA-Z]", "k"));
Assert.assertFalse(checkMatches("[^0-9]", "5")); // 字符不能是0 - 9

输出

1
2
3
4
5
6
b	not matches: [^abc]
m not matches: [^a-z]
O not matches: [^A-Z]
K not matches: [^a-zA-Z]
k not matches: [^a-zA-Z]
5 not matches: [^0-9]

限制字符数量 - {}

如果想要控制字符出现的次数,可以使用{}

字符 描述
{n} n 是一个非负整数。匹配确定的 n 次。
{n,} n 是一个非负整数。至少匹配 n 次。
{n,m} m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。

例 限制字符出现次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// {n}: n 是一个非负整数。匹配确定的 n 次。
checkMatches("ap{1}", "a");
checkMatches("ap{1}", "ap");
checkMatches("ap{1}", "app");
checkMatches("ap{1}", "apppppppppp");

// {n,}: n 是一个非负整数。至少匹配 n 次。
checkMatches("ap{1,}", "a");
checkMatches("ap{1,}", "ap");
checkMatches("ap{1,}", "app");
checkMatches("ap{1,}", "apppppppppp");

// {n,m}: m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。
checkMatches("ap{2,5}", "a");
checkMatches("ap{2,5}", "ap");
checkMatches("ap{2,5}", "app");
checkMatches("ap{2,5}", "apppppppppp");

输出

1
2
3
4
5
6
7
8
9
10
11
12
a	not matches: ap{1}
ap matches: ap{1}
app not matches: ap{1}
apppppppppp not matches: ap{1}
a not matches: ap{1,}
ap matches: ap{1,}
app matches: ap{1,}
apppppppppp matches: ap{1,}
a not matches: ap{2,5}
ap not matches: ap{2,5}
app matches: ap{2,5}
apppppppppp not matches: ap{2,5}

转义字符 - /

如果想要查找元字符本身,你需要使用转义符,使得正则引擎将其视作一个普通字符,而不是一个元字符去处理。

1
2
3
4
5
6
* 的转义字符:\*
+ 的转义字符:\+
? 的转义字符:\?
^ 的转义字符:\^
$ 的转义字符:\$
. 的转义字符:\.

如果是转义符\本身,你也需要使用\\

指定表达式字符串的开始和结尾 - ^、$

如果希望匹配的字符串必须以特定字符串开头,可以使用^

注:请特别留意,这里的^ 一定要和 [^] 中的 “^” 区分。

例 限制字符串头部

1
2
Assert.assertTrue(checkMatches("^app[a-z]{0,}", "apple")); // 字符串必须以app开头
Assert.assertFalse(checkMatches("^app[a-z]{0,}", "aplause"));

输出

1
2
apple	matches: ^app[a-z]{0,}
aplause not matches: ^app[a-z]{0,}

如果希望匹配的字符串必须以特定字符串开头,可以使用$

例 限制字符串尾部

1
2
Assert.assertTrue(checkMatches("[a-z]{0,}ing$", "playing")); // 字符串必须以ing结尾
Assert.assertFalse(checkMatches("[a-z]{0,}ing$", "long"));

输出

1
2
playing	matches: [a-z]{0,}ing$
long not matches: [a-z]{0,}ing$

等价字符

等价字符,顾名思义,就是对于基本元字符表达的一种简化(等价字符的功能都可以通过基本元字符来实现)。

在没有掌握基本元字符之前,可以先不用理会,因为很容易把人绕晕。

等价字符的好处在于简化了基本元字符的写法。

表示某一类型字符的等价字符

下表中的等价字符都表示某一类型的字符。

字符 描述
. 匹配除 \n 之外的任何单个字符。
\d 匹配一个数字字符。等价于[0-9]。
\D 匹配一个非数字字符。等价于[^0-9]。
\w 匹配包括下划线的任何单词字符。类似但不等价于[A-Za-z0-9_],这里的单词字符指的是 Unicode 字符集。
\W 匹配任何非单词字符。
\s 匹配任何不可见字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]
\S 匹配任何可见字符。等价于[ \f\n\r\t\v]

案例 基本等价字符的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 匹配除“\n”之外的任何单个字符
Assert.assertTrue(checkMatches(".{1,}", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"));
Assert.assertTrue(checkMatches(".{1,}", "~!@#$%^&*()+`-=[]{};:<>,./?|\\"));
Assert.assertFalse(checkMatches(".", "\n"));
Assert.assertFalse(checkMatches("[^\n]", "\n"));

// 匹配一个数字字符。等价于[0-9]
Assert.assertTrue(checkMatches("\\d{1,}", "0123456789"));
// 匹配一个非数字字符。等价于[^0-9]
Assert.assertFalse(checkMatches("\\D{1,}", "0123456789"));

// 匹配包括下划线的任何单词字符。类似但不等价于“[A-Za-z0-9_]”,这里的单词字符指的是Unicode字符集
Assert.assertTrue(checkMatches("\\w{1,}", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"));
Assert.assertFalse(checkMatches("\\w{1,}", "~!@#$%^&*()+`-=[]{};:<>,./?|\\"));
// 匹配任何非单词字符
Assert.assertFalse(checkMatches("\\W{1,}", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"));
Assert.assertTrue(checkMatches("\\W{1,}", "~!@#$%^&*()+`-=[]{};:<>,./?|\\"));

// 匹配任何不可见字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]
Assert.assertTrue(checkMatches("\\s{1,}", " \f\r\n\t"));
// 匹配任何可见字符。等价于[^ \f\n\r\t\v]
Assert.assertFalse(checkMatches("\\S{1,}", " \f\r\n\t"));

输出

1
2
3
4
5
6
7
8
9
10
11
12
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_	matches: .{1,}
~!@#$%^&*()+`-=[]{};:<>,./?|\\ matches: .{1,}
\n not matches: .
\n not matches: [^\n]
0123456789 matches: \\d{1,}
0123456789 not matches: \\D{1,}
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_ matches: \\w{1,}
~!@#$%^&*()+`-=[]{};:<>,./?|\\ not matches: \\w{1,}
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_ not matches: \\W{1,}
~!@#$%^&*()+`-=[]{};:<>,./?|\\ matches: \\W{1,}
\f\r\n\t matches: \\s{1,}
\f\r\n\t not matches: \\S{1,}

限制字符数量的等价字符

在基本元字符章节中,已经介绍了限制字符数量的基本元字符 - {}

此外,还有 *+? 这个三个为了简化写法而出现的等价字符,我们来认识一下。

字符 描述
* 匹配前面的子表达式零次或多次。等价于{0,}。
+ 匹配前面的子表达式一次或多次。等价于{1,}。
? 匹配前面的子表达式零次或一次。等价于 {0,1}。

案例 限制字符数量的等价字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// *: 匹配前面的子表达式零次或多次。* 等价于{0,}。
checkMatches("ap*", "a");
checkMatches("ap*", "ap");
checkMatches("ap*", "app");
checkMatches("ap*", "apppppppppp");

// +: 匹配前面的子表达式一次或多次。+ 等价于 {1,}。
checkMatches("ap+", "a");
checkMatches("ap+", "ap");
checkMatches("ap+", "app");
checkMatches("ap+", "apppppppppp");

// ?: 匹配前面的子表达式零次或一次。? 等价于 {0,1}。
checkMatches("ap?", "a");
checkMatches("ap?", "ap");
checkMatches("ap?", "app");
checkMatches("ap?", "apppppppppp");

输出

1
2
3
4
5
6
7
8
9
10
11
12
a	matches: ap*
ap matches: ap*
app matches: ap*
apppppppppp matches: ap*
a not matches: ap+
ap matches: ap+
app matches: ap+
apppppppppp matches: ap+
a matches: ap?
ap matches: ap?
app not matches: ap?
apppppppppp not matches: ap?

元字符优先级顺序

正则表达式从左到右进行计算,并遵循优先级顺序,这与算术表达式非常类似。

下表从最高到最低说明了各种正则表达式运算符的优先级顺序:

运算符 说明
\\ 转义符
(), (?:), (?=), [] 括号和中括号
\*, +, ?, {n}, {n,}, {n,m} 限定符
^, $, * 任何元字符、任何字符 定位点和序列
` `

字符具有高于替换运算符的优先级,使得“m|food”匹配“m”或“food”。若要匹配“mood”或“food”,请使用括号创建子表达式,从而产生“(m|f)ood”。

分组构造

在基本元字符章节,提到了 () 字符可以用来对表达式分组。实际上分组还有更多复杂的用法。

所谓分组构造,是用来描述正则表达式的子表达式,用于捕获字符串中的子字符串。

捕获与非捕获

下表为分组构造中的捕获和非捕获分类。

表达式 描述 捕获或非捕获
(exp) 匹配的子表达式 捕获
(?<name>exp) 命名的反向引用 捕获
(?:exp) 非捕获组 非捕获
(?=exp) 零宽度正预测先行断言 非捕获
(?!exp) 零宽度负预测先行断言 非捕获
(?<=exp) 零宽度正回顾后发断言 非捕获
(?<!exp) 零宽度负回顾后发断言 非捕获

注:Java 正则引擎不支持平衡组。

反向引用

带编号的反向引用

带编号的反向引用使用以下语法:\number

其中number 是正则表达式中捕获组的序号位置。 例如,\4 匹配第四个捕获组的内容。 如果正则表达式模式中未定义number,则将发生分析错误

例 匹配重复的单词和紧随每个重复的单词的单词(不命名子表达式)

1
2
3
// (\w+)\s\1\W(\w+) 匹配重复的单词和紧随每个重复的单词的单词
Assert.assertTrue(findAll("(\\w+)\\s\\1\\W(\\w+)",
"He said that that was the the correct answer.") > 0);

输出

1
2
3
regex = (\w+)\s\1\W(\w+), content: He said that that was the the correct answer.
[1th] start: 8, end: 21, group: that that was
[2th] start: 22, end: 37, group: the the correct

说明

(\w+): 匹配一个或多个单词字符。
\s: 与空白字符匹配。
\1: 匹配第一个组,即(\w+)。
\W: 匹配包括空格和标点符号的一个非单词字符。 这样可以防止正则表达式模式匹配从第一个捕获组的单词开头的单词。

命名的反向引用

命名后向引用通过使用下面的语法进行定义:\k<name >

例 匹配重复的单词和紧随每个重复的单词的单词(命名子表达式)

1
2
3
// (?<duplicateWord>\w+)\s\k<duplicateWord>\W(?<nextWord>\w+) 匹配重复的单词和紧随每个重复的单词的单词
Assert.assertTrue(findAll("(?<duplicateWord>\\w+)\\s\\k<duplicateWord>\\W(?<nextWord>\\w+)",
"He said that that was the the correct answer.") > 0);

输出

1
2
3
regex = (?<duplicateWord>\w+)\s\k<duplicateWord>\W(?<nextWord>\w+), content: He said that that was the the correct answer.
[1th] start: 8, end: 21, group: that that was
[2th] start: 22, end: 37, group: the the correct

说明

(?<duplicateWord>\w+): 匹配一个或多个单词字符。 命名此捕获组 duplicateWord。
\s: 与空白字符匹配。
\k<duplicateWord>: 匹配名为 duplicateWord 的捕获的组。
\W: 匹配包括空格和标点符号的一个非单词字符。 这样可以防止正则表达式模式匹配从第一个捕获组的单词开头的单词。
(?<nextWord>\w+): 匹配一个或多个单词字符。 命名此捕获组 nextWord。

非捕获组

(?:exp) 表示当一个限定符应用到一个组,但组捕获的子字符串并非所需时,通常会使用非捕获组构造。

例 匹配以.结束的语句。

1
2
// 匹配由句号终止的语句。
Assert.assertTrue(findAll("(?:\\b(?:\\w+)\\W*)+\\.", "This is a short sentence. Never end") > 0);

输出

1
2
regex = (?:\b(?:\w+)\W*)+\., content: This is a short sentence. Never end
[1th] start: 0, end: 25, group: This is a short sentence.

零宽断言

用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\b,^,$那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言。

表达式 描述
(?=exp) 匹配 exp 前面的位置
(?<=exp) 匹配 exp 后面的位置
(?!exp) 匹配后面跟的不是 exp 的位置
(?<!exp) 匹配前面不是 exp 的位置

匹配 exp 前面的位置

(?=exp) 表示输入字符串必须匹配子表达式中的正则表达式模式,尽管匹配的子字符串未包含在匹配结果中。

1
2
3
4
5
// \b\w+(?=\sis\b) 表示要捕获is之前的单词
Assert.assertTrue(findAll("\\b\\w+(?=\\sis\\b)", "The dog is a Malamute.") > 0);
Assert.assertFalse(findAll("\\b\\w+(?=\\sis\\b)", "The island has beautiful birds.") > 0);
Assert.assertFalse(findAll("\\b\\w+(?=\\sis\\b)", "The pitch missed home plate.") > 0);
Assert.assertTrue(findAll("\\b\\w+(?=\\sis\\b)", "Sunday is a weekend day.") > 0);

输出

1
2
3
4
5
6
7
8
regex = \b\w+(?=\sis\b), content: The dog is a Malamute.
[1th] start: 4, end: 7, group: dog
regex = \b\w+(?=\sis\b), content: The island has beautiful birds.
not found
regex = \b\w+(?=\sis\b), content: The pitch missed home plate.
not found
regex = \b\w+(?=\sis\b), content: Sunday is a weekend day.
[1th] start: 0, end: 6, group: Sunday

说明

\b: 在单词边界处开始匹配。

\w+: 匹配一个或多个单词字符。

(?=\sis\b): 确定单词字符是否后接空白字符和字符串“is”,其在单词边界处结束。 如果如此,则匹配成功。

匹配 exp 后面的位置

(?<=exp) 表示子表达式不得在输入字符串当前位置左侧出现,尽管子表达式未包含在匹配结果中。零宽度正回顾后发断言不会回溯。

1
2
// (?<=\b20)\d{2}\b 表示要捕获以20开头的数字的后面部分
Assert.assertTrue(findAll("(?<=\\b20)\\d{2}\\b", "2010 1999 1861 2140 2009") > 0);

输出

1
2
3
regex = (?<=\b20)\d{2}\b, content: 2010 1999 1861 2140 2009
[1th] start: 2, end: 4, group: 10
[2th] start: 22, end: 24, group: 09

说明

\d{2}: 匹配两个十进制数字。

{?<=\b20): 如果两个十进制数字的字边界以小数位数“20”开头,则继续匹配。

\b: 在单词边界处结束匹配。

匹配后面跟的不是 exp 的位置

(?!exp) 表示输入字符串不得匹配子表达式中的正则表达式模式,尽管匹配的子字符串未包含在匹配结果中。

例 捕获未以“un”开头的单词

1
2
// \b(?!un)\w+\b 表示要捕获未以“un”开头的单词
Assert.assertTrue(findAll("\\b(?!un)\\w+\\b", "unite one unethical ethics use untie ultimate") > 0);

输出

1
2
3
4
5
regex = \b(?!un)\w+\b, content: unite one unethical ethics use untie ultimate
[1th] start: 6, end: 9, group: one
[2th] start: 20, end: 26, group: ethics
[3th] start: 27, end: 30, group: use
[4th] start: 37, end: 45, group: ultimate

说明

\b: 在单词边界处开始匹配。

(?!un): 确定接下来的两个的字符是否为“un”。 如果没有,则可能匹配。

\w+: 匹配一个或多个单词字符。

\b: 在单词边界处结束匹配。

匹配前面不是 exp 的位置

(?<!exp) 表示子表达式不得在输入字符串当前位置的左侧出现。 但是,任何不匹配子表达式 的子字符串不包含在匹配结果中。

例 捕获任意工作日

1
2
3
4
5
6
// (?<!(Saturday|Sunday) )\b\w+ \d{1,2}, \d{4}\b 表示要捕获任意工作日(即周一到周五)
Assert.assertTrue(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Monday February 1, 2010") > 0);
Assert.assertTrue(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Wednesday February 3, 2010") > 0);
Assert.assertFalse(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Saturday February 6, 2010") > 0);
Assert.assertFalse(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Sunday February 7, 2010") > 0);
Assert.assertTrue(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Monday, February 8, 2010") > 0);

输出

1
2
3
4
5
6
7
8
9
10
regex = (?<!(Saturday|Sunday) )\b\w+ \d{1,2}, \d{4}\b, content: Monday February 1, 2010
[1th] start: 7, end: 23, group: February 1, 2010
regex = (?<!(Saturday|Sunday) )\b\w+ \d{1,2}, \d{4}\b, content: Wednesday February 3, 2010
[1th] start: 10, end: 26, group: February 3, 2010
regex = (?<!(Saturday|Sunday) )\b\w+ \d{1,2}, \d{4}\b, content: Saturday February 6, 2010
not found
regex = (?<!(Saturday|Sunday) )\b\w+ \d{1,2}, \d{4}\b, content: Sunday February 7, 2010
not found
regex = (?<!(Saturday|Sunday) )\b\w+ \d{1,2}, \d{4}\b, content: Monday, February 8, 2010
[1th] start: 8, end: 24, group: February 8, 2010

贪婪与懒惰

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。以这个表达式为例:a.*b,它将会匹配最长的以 a 开始,以 b 结束的字符串。如果用它来搜索 aabab 的话,它会匹配整个字符串 aabab。这被称为贪婪匹配。

有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?。这样.*?就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。

表达式 描述
*? 重复任意次,但尽可能少重复
+? 重复 1 次或更多次,但尽可能少重复
?? 重复 0 次或 1 次,但尽可能少重复
{n,m}? 重复 n 到 m 次,但尽可能少重复
{n,}? 重复 n 次以上,但尽可能少重复

例 Java 正则中贪婪与懒惰的示例

1
2
3
4
5
6
7
8
9
// 贪婪匹配
Assert.assertTrue(findAll("a\\w*b", "abaabaaabaaaab") > 0);

// 懒惰匹配
Assert.assertTrue(findAll("a\\w*?b", "abaabaaabaaaab") > 0);
Assert.assertTrue(findAll("a\\w+?b", "abaabaaabaaaab") > 0);
Assert.assertTrue(findAll("a\\w??b", "abaabaaabaaaab") > 0);
Assert.assertTrue(findAll("a\\w{0,4}?b", "abaabaaabaaaab") > 0);
Assert.assertTrue(findAll("a\\w{3,}?b", "abaabaaabaaaab") > 0);

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
regex = a\w*b, content: abaabaaabaaaab
[1th] start: 0, end: 14, group: abaabaaabaaaab
regex = a\w*?b, content: abaabaaabaaaab
[1th] start: 0, end: 2, group: ab
[2th] start: 2, end: 5, group: aab
[3th] start: 5, end: 9, group: aaab
[4th] start: 9, end: 14, group: aaaab
regex = a\w+?b, content: abaabaaabaaaab
[1th] start: 0, end: 5, group: abaab
[2th] start: 5, end: 9, group: aaab
[3th] start: 9, end: 14, group: aaaab
regex = a\w??b, content: abaabaaabaaaab
[1th] start: 0, end: 2, group: ab
[2th] start: 2, end: 5, group: aab
[3th] start: 6, end: 9, group: aab
[4th] start: 11, end: 14, group: aab
regex = a\w{0,4}?b, content: abaabaaabaaaab
[1th] start: 0, end: 2, group: ab
[2th] start: 2, end: 5, group: aab
[3th] start: 5, end: 9, group: aaab
[4th] start: 9, end: 14, group: aaaab
regex = a\w{3,}?b, content: abaabaaabaaaab
[1th] start: 0, end: 5, group: abaab
[2th] start: 5, end: 14, group: aaabaaaab

说明

本例中代码展示的是使用不同贪婪或懒惰策略去查找字符串”abaabaaabaaaab” 中匹配以”a”开头,以”b”结尾的所有子字符串

请从输出结果中,细细体味使用不同的贪婪或懒惰策略,对于匹配子字符串有什么影响。

最实用的正则

校验中文

描述:校验字符串中只能有中文字符(不包括中文标点符号)。中文字符的 Unicode 编码范围是\u4e00 到 \u9fa5。

如有兴趣,可以参考百度百科-Unicode

1
^[\u4e00-\u9fa5]+$

匹配: 春眠不觉晓

不匹配:春眠不觉晓,

校验身份证号码

描述:身份证为 15 位或 18 位。15 位是第一代身份证。从 1999 年 10 月 1 日起,全国实行公民身份证号码制度,居民身份证编号由原 15 位升至 18 位。

15 位身份证

描述:由 15 位数字组成。排列顺序从左至右依次为:六位数字地区码;六位数字出生日期;三位顺序号,其中 15 位男为单数,女为双数。

18 位身份证

描述:由十七位数字本体码和一位数字校验码组成。排列顺序从左至右依次为:六位数字地区码;八位数字出生日期;三位数字顺序码和一位数字校验码(也可能是 X)。

身份证号含义详情请见:百度百科-居民身份证号码

地区码(6 位)

1
(1[1-5]|2[1-3]|3[1-7]|4[1-3]|5[0-4]|6[1-5])\d{4}

出生日期(8 位)

注:下面的是 18 位身份证的有效出生日期,如果是 15 位身份证,只要将第一个\d{4}改为\d{2}即可。

1
((\d{4}((0[13578]|1[02])(0[1-9]|[12]\d|3[01])|(0[13456789]|1[012])(0[1-9]|[12]\d|30)|02(0[1-9]|1\d|2[0-8])))|([02468][048]|[13579][26])0229)

15 位有效身份证

1
^((1[1-5]|2[1-3]|3[1-7]|4[1-3]|5[0-4]|6[1-5])\d{4})((\d{2}((0[13578]|1[02])(0[1-9]|[12]\d|3[01])|(0[13456789]|1[012])(0[1-9]|[12]\d|30)|02(0[1-9]|1\d|2[0-8])))|([02468][048]|[13579][26])0229)(\d{3})$

匹配:110001700101031

不匹配:110001701501031

18 位有效身份证

1
^((1[1-5]|2[1-3]|3[1-7]|4[1-3]|5[0-4]|6[1-5])\d{4})((\d{4}((0[13578]|1[02])(0[1-9]|[12]\d|3[01])|(0[13456789]|1[012])(0[1-9]|[12]\d|30)|02(0[1-9]|1\d|2[0-8])))|([02468][048]|[13579][26])0229)(\d{3}(\d|X))$

匹配:110001199001010310 | 11000019900101015X

不匹配:990000199001010310 | 110001199013010310

校验有效用户名、密码

描述:长度为 6-18 个字符,允许输入字母、数字、下划线,首字符必须为字母。

1
^[a-zA-Z]\w{5,17}$

匹配:he_llo@worl.d.com | hel.l-o@wor-ld.museum | h1ello@123.com

不匹配:hello@worl_d.com | he&llo@world.co1 | .hello@wor#.co.uk

校验邮箱

描述:不允许使用 IP 作为域名,如 : hello@154.145.68.12

@符号前的邮箱用户和.符号前的域名(domain)必须满足以下条件:

  • 字符只能是英文字母、数字、下划线_.-
  • 首字符必须为字母或数字;
  • _.- 不能连续出现。

域名的根域只能为字母,且至少为两个字符。

1
^[A-Za-z0-9](([_\.\-]?[a-zA-Z0-9]+)*)@([A-Za-z0-9]+)(([\.\-]?[a-zA-Z0-9]+)*)\.([A-Za-z]{2,})$

匹配:he_llo@worl.d.com | hel.l-o@wor-ld.museum | h1ello@123.com

不匹配:hello@worl_d.com | he&llo@world.co1 | .hello@wor#.co.uk

校验 URL

描述:校验 URL。支持 http、https、ftp、ftps。

1
^(ht|f)(tp|tps)\://[a-zA-Z0-9\-\.]+\.([a-zA-Z]{2,3})?(/\S*)?$

匹配:http://google.com/help/me | http://www.google.com/help/me/ | https://www.google.com/help.asp | ftp://www.google.com | ftps://google.org

不匹配:http://un/www.google.com/index.asp

校验时间

描述:校验时间。时、分、秒必须是有效数字,如果数值不是两位数,十位需要补零。

1
^([0-1][0-9]|[2][0-3]):([0-5][0-9])$

匹配:00:00:00 | 23:59:59 | 17:06:30

不匹配:17:6:30 | 24:16:30

校验日期

描述:校验日期。日期满足以下条件:

  • 格式 yyyy-MM-dd 或 yyyy-M-d
  • 连字符可以没有或是“-”、“/”、“.”之一
  • 闰年的二月可以有 29 日;而平年不可以。
  • 一、三、五、七、八、十、十二月为 31 日。四、六、九、十一月为 30 日。
1
^(?:(?!0000)[0-9]{4}([-/.]?)(?:(?:0?[1-9]|1[0-2])\1(?:0?[1-9]|1[0-9]|2[0-8])|(?:0?[13-9]|1[0-2])\1(?:29|30)|(?:0?[13578]|1[02])\1(?:31))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)([-/.]?)0?2\2(?:29))$

匹配:2016/1/1 | 2016/01/01 | 20160101 | 2016-01-01 | 2016.01.01 | 2000-02-29

不匹配:2001-02-29 | 2016/12/32 | 2016/6/31 | 2016/13/1 | 2016/0/1

校验中国手机号码

描述:中国手机号码正确格式:11 位数字。

移动有 16 个号段:134、135、136、137、138、139、147、150、151、152、157、158、159、182、187、188。其中 147、157、188 是 3G 号段,其他都是 2G 号段。联通有 7 种号段:130、131、132、155、156、185、186。其中 186 是 3G(WCDMA)号段,其余为 2G 号段。电信有 4 个号段:133、153、180、189。其中 189 是 3G 号段(CDMA2000),133 号段主要用作无线网卡号。总结:13 开头手机号 0-9;15 开头手机号 0-3、5-9;18 开头手机号 0、2、5-9。

此外,中国在国际上的区号为 86,所以手机号开头有+86、86 也是合法的。

以上信息来源于 百度百科-手机号

1
^((\+)?86\s*)?((13[0-9])|(15([0-3]|[5-9]))|(18[0,2,5-9]))\d{8}$

匹配:+86 18012345678 | 86 18012345678 | 15812345678

不匹配:15412345678 | 12912345678 | 180123456789

校验中国固话号码

描述:固话号码,必须加区号(以 0 开头)。
3 位有效区号:010、020~029,固话位数为 8 位。
4 位有效区号:03xx 开头到 09xx,固话位数为 7。

如果想了解更详细的信息,请参考 百度百科-电话区号

1
^(010|02[0-9])(\s|-)\d{8}|(0[3-9]\d{2})(\s|-)\d{7}$

匹配:010-12345678 | 010 12345678 | 0512-1234567 | 0512 1234567

不匹配:1234567 | 12345678

校验 IPv4 地址

描述:IP 地址是一个 32 位的二进制数,通常被分割为 4 个“8 位二进制数”(也就是 4 个字节)。IP 地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d 都是 0~255 之间的十进制整数。

1
^([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])$

匹配:0.0.0.0 | 255.255.255.255 | 127.0.0.1

不匹配:10.10.10 | 10.10.10.256

校验 IPv6 地址

描述:IPv6 的 128 位地址通常写成 8 组,每组为四个十六进制数的形式。

IPv6 地址可以表示为以下形式:

显然,IPv6 地址的表示方式很复杂。你也可以参考

百度百科-IPv6

Stack overflow 上的 IPv6 正则表达高票答案

1
(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))

匹配:1:2:3:4:5:6:7:8 | 1:: | 1::8 | 1::6:7:8 | 1::5:6:7:8 | 1::4:5:6:7:8 | 1::3:4:5:6:7:8 | ::2:3:4:5:6:7:8 | 1:2:3:4:5:6:7:: | 1:2:3:4:5:6::8 | 1:2:3:4:5::8 | 1:2:3:4::8 | 1:2:3::8 | 1:2::8 | 1::8 | ::8 | fe80::7:8%1 | ::255.255.255.255 | 2001:db8:3:4::192.0.2.33 | 64:ff9b::192.0.2.33

不匹配:1.2.3.4.5.6.7.8 | 1::2::3

特定字符

匹配长度为 3 的字符串:^.{3}$

匹配由 26 个英文字母组成的字符串:^[A-Za-z]+$

匹配由 26 个大写英文字母组成的字符串:^[A-Z]+$

匹配由 26 个小写英文字母组成的字符串:^[a-z]+$

匹配由数字和 26 个英文字母组成的字符串:^[A-Za-z0-9]+$

匹配由数字、26 个英文字母或者下划线组成的字符串:^\w+$

特定数字

匹配正整数:^[1-9]\d*$

匹配负整数:^-[1-9]\d*$

匹配整数:^(-?[1-9]\d*)|0$

匹配正浮点数:^[1-9]\d*\.\d+|0\.\d+$

匹配负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$

匹配浮点数:^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$

速查元字符字典

为了方便快查正则的元字符含义,在本节根据元字符的功能集中罗列正则的各种元字符。

限定符

字符 描述
* 匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。* 等价于{0,}。
+ 匹配前面的子表达式一次或多次。例如,’zo+’ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。
? 匹配前面的子表达式零次或一次。例如,”do(es)?” 可以匹配 “do” 或 “does” 中的”do” 。? 等价于 {0,1}。
{n} n 是一个非负整数。匹配确定的 n 次。例如,’o{2}’ 不能匹配 “Bob” 中的 ‘o’,但是能匹配 “food” 中的两个 o。
{n,} n 是一个非负整数。至少匹配 n 次。例如,’o{2,}’ 不能匹配 “Bob” 中的 ‘o’,但能匹配 “foooood” 中的所有 o。’o{1,}’ 等价于 ‘o+’。’o{0,}’ 则等价于 ‘o*‘。
{n,m} m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。例如,”o{1,3}” 将匹配 “fooooood” 中的前三个 o。’o{0,1}’ 等价于 ‘o?’。请注意在逗号和两个数之间不能有空格。

定位符

字符 描述
^ 匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与 \n 或 \r 之后的位置匹配。
$ 匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与 \n 或 \r 之前的位置匹配。
\b 匹配一个字边界,即字与空格间的位置。
\B 非字边界匹配。

非打印字符

字符 描述
\cx 匹配由 x 指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 ‘c’ 字符。
\f 匹配一个换页符。等价于 \x0c 和 \cL。
\n 匹配一个换行符。等价于 \x0a 和 \cJ。
\r 匹配一个回车符。等价于 \x0d 和 \cM。
\s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。
\S 匹配任何非空白字符。等价于 [ \f\n\r\t\v]。
\t 匹配一个制表符。等价于 \x09 和 \cI。
\v 匹配一个垂直制表符。等价于 \x0b 和 \cK。

分组

表达式 描述
(exp) 匹配的子表达式。()中的内容就是子表达式。
(?<name>exp) 命名的子表达式(反向引用)。
(?:exp) 非捕获组,表示当一个限定符应用到一个组,但组捕获的子字符串并非所需时,通常会使用非捕获组构造。
(?=exp) 匹配 exp 前面的位置。
(?<=exp) 匹配 exp 后面的位置。
(?!exp) 匹配后面跟的不是 exp 的位置。
(?<!exp) 匹配前面不是 exp 的位置。

特殊符号

字符 描述
\ 将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, ‘n’ 匹配字符 ‘n’。’\n’ 匹配换行符。序列 ‘\‘ 匹配 “",而 ‘(‘ 则匹配 “(“。
| 指明两项之间的一个选择。
[] 匹配方括号范围内的任意一个字符。形式如:[xyz]、[^xyz]、[a-z]、[^a-z]、[x,y,z]

参考资料

超文本传输协议 HTTP

超文本传输协议(HTTP)是一个用于传输超媒体文档(例如 HTML)的应用层协议。

HTTP 简介

HTTP 是什么

超文本传输协议(HTTP)是一个用于传输超媒体文档(例如 HTML)的应用层协议。HTTP 是 浏览器与服务器之间的数据传送协议。HTTP 遵循经典的客户端-服务端模型,客户端打开一个连接以发出请求,然后等待它收到服务器端响应。HTTP 是无状态协议,这意味着服务器不会在两个请求之间保留任何数据(状态)。该协议虽然通常基于 TCP/IP 层,但可以在任何可靠的传输层上使用;也就是说,不像 UDP,它是一个不会静默丢失消息的协议。

HTTP 是由 IETF(Internet Engineering Task Force,互联网工程工作小组) 和 W3C(World Wide Web Consortium,万维网协会) 共同合作制订的,它们发布了一系列的RFC(Request For Comments),其中最著名的是 RFC 2616,它定义了HTTP /1.1

img

HTTP 协议特点

  • 无连接的 - 无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
  • 无状态的 - HTTP 协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
  • 媒体独立的 - 这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过 HTTP 发送。客户端以及服务器指定使用适合的 MIME-type 内容类型。
  • C/S 模型的 - 基于 Client/Server 模型工作。

HTTP 版本特性

HTTP 1.1

HTTP1.0 和 HTTP 1.1 主要区别如下:

  • 缓存处理,在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。
  • 带宽优化及网络连接的使用
  • 错误通知的管理 - HTTP1.1 中新增了 24 个错误状态响应码。
  • Host 头处理
    • HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。
    • 随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个 IP 地址。HTTP1.1 的请求消息和响应消息都应支持 Host 头域,且请求消息中如果没有 Host 头域会报告一个错误(400 Bad Request)。
  • 长连接,HTTP 1.1 支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个 TCP 连接上可以传送多个 HTTP 请求和响应,减少了建立和关闭连接的消耗和延迟,在 HTTP1.1 中默认开启 Connection: keep-alive,一定程度上弥补了 HTTP1.0 每次请求都要创建连接的缺点。

HTTP 2.0

HTTP/2 在 HTTP/1.1 有几处基本的不同:

  • HTTP/2 是二进制协议而不是文本协议。不再可读,也不可无障碍的手动创建,改善的优化技术现在可被实施。
  • 这是一个复用协议。并行的请求能在同一个链接中处理,移除了 HTTP/1.x 中顺序和阻塞的约束。
  • 压缩了 headers。因为 headers 在一系列请求中常常是相似的,其移除了重复和传输重复数据的成本。
  • 其允许服务器在客户端缓存中填充数据,通过一个叫服务器推送的机制来提前请求。

工作原理

HTTP 工作于 Client/Server 模型上。

客户端和服务器之间的通信采用 request/response 机制。

客户端是终端(可以是浏览器、爬虫程序等),服务器是网站的 Web 服务器。

一次 HTTP 操作称为一个事务,其工作过程大致可分为四步:

  1. 建立连接 - 首先,客户端和服务器需要建立一个到服务器指定端口(默认端口号为 80)的 TCP 连接(注:虽然 HTTP 采用 TCP 连接是最流行的方式,但是 RFC 并没有指定一定要采用这种网络传输方式。)。
  2. 发送请求信息 - 客户端向服务器发送请求。请求方式的格式为,统一资源标识符、协议版本号,后边是 MIME 信息包括请求修饰符
  3. 发送响应信息 - 服务器监听指定接口是否收到请求,一旦发现请求,处理后,返回响应结果给客户端。其格式为一个状态行包括信息的协议版本号、一个成功或错误的代码,后边是 MIME 信息包括服务器信息、实体信息和可能的内容。
  4. 关闭连接 - 客户端根据响应,显示结果给用户,最后关闭连接。

HTTP 优化

影响一个 HTTP 网络请求的因素主要有两个:带宽和延迟。

  • 带宽:如果说我们还停留在拨号上网的阶段,带宽可能会成为一个比较严重影响请求的问题,但是现在网络基础建设已经使得带宽得到极大的提升,我们不再会担心由带宽而影响网速,那么就只剩下延迟了。

  • 延迟:

    • 浏览器阻塞(HOL blocking):浏览器会因为一些原因阻塞请求。浏览器对于同一个域名,同时只能有 4 个连接(这个根据浏览器内核不同可能会有所差异),超过浏览器最大连接数限制,后续请求就会被阻塞。
    • DNS 查询(DNS Lookup):浏览器需要知道目标服务器的 IP 才能建立连接。将域名解析为 IP 的这个系统就是 DNS。这个通常可以利用 DNS 缓存结果来达到减少这个时间的目的。
    • 建立连接(Initial connection):HTTP 是基于 TCP 协议的,浏览器最快也要在第三次握手时才能捎带 HTTP 请求报文,达到真正的建立连接,但是这些连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大。

HTTP 报文

HTTP 是基于客户端/服务端(C/S)的架构模型,通过一个可靠的链接来交换信息,是一个无状态的请求/响应协议。

一个 HTTP”客户端”是一个应用程序(Web 浏览器或其他任何客户端),通过连接到服务器达到向服务器发送一个或多个 HTTP 的请求的目的。

一个 HTTP”服务器”同样也是一个应用程序(通常是一个 Web 服务,如 Apache Web 服务器或 IIS 服务器等),通过接收客户端的请求并向客户端发送 HTTP 响应数据。

HTTP 使用统一资源标识符(Uniform Resource Identifiers, URI)来传输数据和建立连接。

一旦建立连接后,数据消息就通过类似 Internet 邮件所使用的格式[RFC5322]和多用途 Internet 邮件扩展(MIME)[RFC2045]来传送。

img
以下是使用 wireshark 抓取的一个实际访问百度首页的 HTTP GET 报文:

img
可以清楚的看到它的层级结构如下图,经过了层层的包装。

img

HTTP 请求报文

客户端发送一个 HTTP 请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。

img

HTTP 请求报文由以下元素组成:

  • 一个 HTTP 的method,经常是由一个动词像GET, POST 或者一个名词像OPTIONSHEAD来定义客户端的动作行为。通常客户端的操作都是获取资源(GET 方法)或者发送HTML form表单值(POST 方法),虽然在一些情况下也会有其他操作。
  • 要获取的资源的路径,通常是上下文中就很明显的元素资源的 URL,它没有protocolhttp://),domaindeveloper.mozilla.org),或是 TCP 的port(HTTP 一般在 80 端口)。
  • HTTP 协议版本号。
  • 为服务端表达其他信息的可选头部headers
  • 对于一些像 POST 这样的方法,报文的 body 就包含了发送的资源,这与响应报文的 body 类似。

根据 HTTP 标准,HTTP 请求可以使用多种请求方法。

HTTP1.0 定义了三种请求方法: GET, POSTHEAD方法。

HTTP1.1 新增了五种请求方法:OPTIONS, PUT, DELETE, TRACECONNECT方法。

方法 描述
GET 请求指定的页面信息,并返回实体主体。
HEAD 类似于 get 请求,只不过返回的响应中没有具体的内容,用于获取报头
POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。
PUT 从客户端向服务器传送的数据取代指定的文档的内容。
DELETE 请求服务器删除指定的页面。
CONNECT HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。
OPTIONS 允许客户端查看服务器的性能。
TRACE 回显服务器收到的请求,主要用于测试或诊断。

HTTP 请求消息头

请求消息头 说明
Accept 浏览器支持的格式
Accept-Encoding 支持的编码格式,如(UTF-8,GBK)
Accept-Language 支持的语言类型
User-Agent 浏览器信息
Cookie 记录的是用户当前的状态
Referer 指从哪个页面单击链接进入的页面
HOST 目的地址对应的主机名
Connection 连接类型。如 Keep-Alive 表示长连接,不会断开
Content-Length 内容长度
Content-Type 内容类型

HTTP 响应报文

img

HTTP 响应报文包含了下面的元素:

  • HTTP 协议版本号。
  • 一个状态码(status code),来告知对应请求执行成功或失败,以及失败的原因。
  • 一个状态信息,这个信息是非权威的状态码描述信息,可以由服务端自行设定。
  • HTTP headers,与请求头部类似。
  • 可选项,比起请求报文,响应报文中更常见地包含获取的资源 body。

响应消息头

响应消息头 说明
Allow 服务器支持哪些请求方法(如 GET、POST 等)。
Content-Encoding 文档的编码(Encode)方法。只有在解码之后才可以得到 Content-Type 头指定的内容类型。利用 gzip 压缩文档能够显著地减少 HTML 文档的下载时间。Java 的 GZIPOutputStream 可以很方便地进行 gzip 压缩,但只有 Unix 上的 Netscape 和 Windows 上的 IE 4、IE 5 才支持它。因此,Servlet 应该通过查看 Accept-Encoding 头(即 request.getHeader(“Accept-Encoding”))检查浏览器是否支持 gzip,为支持 gzip 的浏览器返回经 gzip 压缩的 HTML 页面,为其他浏览器返回普通页面。
Content-Length 表示内容长度。只有当浏览器使用持久 HTTP 连接时才需要这个数据。如果你想要利用持久连接的优势,可以把输出文档写入 ByteArrayOutputStram,完成后查看其大小,然后把该值放入 Content-Length 头,最后通过byteArrayStream.writeTo(response.getOutputStream() 发送内容。
Content-Type 表示后面的文档属于什么 MIME 类型。Servlet 默认为 text/plain,但通常需要显式地指定为 text/html。由于经常要设置 Content-Type,因此 HttpServletResponse 提供了一个专用的方法 setContentType。
Date 当前的 GMT 时间。你可以用 setDateHeader 来设置这个头以避免转换时间格式的麻烦。
Expires 应该在什么时候认为文档已经过期,从而不再缓存它?
Last-Modified 文档的最后改动时间。客户可以通过 If-Modified-Since 请求头提供一个日期,该请求将被视为一个条件 GET,只有改动时间迟于指定时间的文档才会返回,否则返回一个 304(Not Modified)状态。Last-Modified 也可用 setDateHeader 方法来设置。
Location 表示客户应当到哪里去提取文档。Location 通常不是直接设置的,而是通过 HttpServletResponse 的 sendRedirect 方法,该方法同时设置状态代码为 302。
Refresh 表示浏览器应该在多少时间之后刷新文档,以秒计。除了刷新当前文档之外,你还可以通过 response.setHeader("Refresh", "5;URL=http://host/path")让浏览器读取指定的页面。 注意这种功能通常是通过设置 HTML 页面 HEAD 区的 <META HTTP-EQUIV="Refresh" CONTENT="5;URL=http://host/path">实现,这是因为,自动刷新或重定向对于那些不能使用 CGI 或 Servlet 的 HTML 编写者十分重要。但是,对于 Servlet 来说,直接设置 Refresh 头更加方便。 注意 Refresh 的意义是”N 秒之后刷新本页面或访问指定页面”,而不是”每隔 N 秒刷新本页面或访问指定页面”。因此,连续刷新要求每次都发送一个 Refresh 头,而发送 204 状态代码则可以阻止浏览器继续刷新,不管是使用 Refresh 头还是 <META HTTP-EQUIV="Refresh" ...>。 注意 Refresh 头不属于 HTTP 1.1 正式规范的一部分,而是一个扩展,但 Netscape 和 IE 都支持它。
Server 服务器名字。Servlet 一般不设置这个值,而是由 Web 服务器自己设置。
Set-Cookie 设置和页面关联的 Cookie。Servlet 不应使用response.setHeader("Set-Cookie", ...),而是应使用 HttpServletResponse 提供的专用方法 addCookie。参见下文有关 Cookie 设置的讨论。
WWW-Authenticate 客户应该在 Authorization 头中提供什么类型的授权信息?在包含 401(Unauthorized)状态行的应答中这个头是必需的。例如,response.setHeader("WWW-Authenticate", "BASIC realm=\"executives\"")。 注意 Servlet 一般不进行这方面的处理,而是让 Web 服务器的专门机制来控制受密码保护页面的访问(例如.htaccess)。

HTTP 响应状态码

当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含 HTTP 状态码的信息头(server header)用以响应浏览器的请求。

HTTP 状态码的英文为 **HTTP Status Code**。

下面是常见的 HTTP 状态码:

  • 200 - 请求成功
  • 301 - 资源(网页等)被永久转移到其它 URL
  • 404 - 请求的资源(网页等)不存在
  • 500 - 内部服务器错误

HTTP 状态码分类

HTTP 状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型,后两个数字没有分类的作用。HTTP 状态码共分为 5 种类型:

分类 分类描述
1xx 信息响应。服务器收到请求,需要请求者继续执行操作
2xx 成功响应。操作被成功接收并处理
3xx 重定向。需要进一步的操作以完成请求
4xx 客户端错误。请求包含语法错误或无法完成请求
5xx 服务器错误。服务器在处理请求的过程中发生了错误

:bell: 更详细的 HTTP 状态码可以参考:

HTTPS

HTTP 是明文传输,HTTPS 通过 SSL\TLS 进行了加密

HTTP 的端口号是 80,HTTPS 是 443

HTTPS 需要到 CA 申请证书,一般免费证书很少,需要交费

HTTPS 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。

由于 Http 是一种无状态的协议,服务器单从网络连接上无从知道客户身份。

会话跟踪是 Web 程序中常用的技术,用来跟踪用户的整个会话。常用会话跟踪技术是 Cookie 与 Session。

HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器,并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

Cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)
  1. 客户端请求服务器,如果服务器需要记录该用户的状态,就是用 response 向客户端浏览器颁发一个 Cookie。
  2. 客户端浏览器会把 Cookie 保存下来。
  3. 当浏览器再请求该网站时,浏览器把该请求的网址连同 Cookie 一同提交给服务器。服务器检查该 Cookie,以此来辨认用户状态。

注:Cookie 功能需要浏览器的支持,如果浏览器不支持 Cookie 或者 Cookie 禁用了,Cookie 功能就会失效。

Java 中把 Cookie 封装成了 javax.servlet.http.Cookie 类。

Cookies 通常设置在 HTTP 头信息中(虽然 JavaScript 也可以直接在浏览器上设置一个 Cookie)。

设置 Cookie 的 Servlet 会发送如下的头信息:

1
2
3
4
5
6
7
HTTP/1.1 200 OK
Date: Fri, 04 Feb 2000 21:03:38 GMT
Server: Apache/1.3.9 (UNIX) PHP/4.0b3
Set-Cookie: name=xyz; expires=Friday, 04-Feb-07 22:03:38 GMT;
path=/; domain=w3cschool.cc
Connection: close
Content-Type: text/html

正如您所看到的,Set-Cookie 头包含了一个名称值对、一个 GMT 日期、一个路径和一个域。名称和值会被 URL 编码。expires 字段是一个指令,告诉浏览器在给定的时间和日期之后”忘记”该 Cookie。

如果浏览器被配置为存储 Cookies,它将会保留此信息直到到期日期。如果用户的浏览器指向任何匹配该 Cookie 的路径和域的页面,它会重新发送 Cookie 到服务器。浏览器的头信息可能如下所示:

1
2
3
4
5
6
7
8
9
GET / HTTP/1.0
Connection: Keep-Alive
User-Agent: Mozilla/4.6 (X11; I; Linux 2.2.6-15apmac ppc)
Host: zink.demon.co.uk:1126
Accept: image/gif, */*
Accept-Encoding: gzip
Accept-Language: en
Accept-Charset: iso-8859-1,*,utf-8
Cookie: name=xyz

Session

不同于 Cookie 保存在客户端浏览器中Session 保存在服务器上

由于 Cookie 以明文的方式存储在本地,而 Cookie 中往往带有用户信息,这样就造成了非常大的安全隐患。

Session 的出现解决了这个问题,Session 可以理解为服务器端开辟的存储空间,里面保存了用户的状态,用户信息以 Session 的形式存储在服务端。当用户请求到来时,服务端可以把用户的请求和用户的 Session 对应起来。那么 Session 是怎么和请求对应起来的呢?答案是通过 Cookie,浏览器在 Cookie 中填充了一个 Session ID 之类的字段用来标识请求。

Session 工作流程

Session 工作流程是这样的:

服务器在创建 Session 的同时,会为该 Session 生成唯一的 Session ID,当浏览器再次发送请求的时候,会将这个 Session ID 带上,服务器接受到请求之后就会依据 Session ID 找到相应的 Session,找到 Session 后,就可以在 Session 中获取或者添加内容了。而这些内容只会保存在服务器中,发到客户端的只有 Session ID,这样相对安全,也节省了网络流量,因为不需要在 Cookie 中存储大量用户信息。该 Cookie 为服务器自动生成的,它的 maxAge 属性一般为-1,表示仅当前浏览器内有效,并且各浏览器窗口间不共享,关闭浏览器就会失效。

Session 创建与存储

那么 Session 在何时何地创建呢?当然还是在服务器端程序运行的过程中创建的,不同语言实现的应用程序有不同的创建 Session 的方法。Tomcat 的 Session 管理器提供了多种持久化方案来存储 Session,通常会采用高性能的存储方式,比如 Redis,并且通过集群部署的方式,防止单点故障,从而提升高可用。同时,Session 有过期时间,因此 Tomcat 会开启后台线程定期的轮询,如果 Session 过期了就将 Session 失效。

Cookie vs. Session 对比如下:

  • 存储位置
    • Cookie 存储在浏览器。
      • 不占用服务器资源。
      • 一些客户端的程序可能会窥探、复制或修改 Cookie 内容,安全风险更大。
    • Session 存储在服务器。
      • 每个用户都会产生一个 Session,如果并发访问的用户非常多,会产生很多的 Session,消耗大量的内存。
      • 对客户端是透明的,不存在敏感信息泄露的危险。
  • 存取方式
    • Cookie 只能保存 ASCII 字符串,如果需要存取 Unicode 字符或二进制数据,需要进行UTF-8GBKBASE64等方式的编码。
    • Session 可以存取任何类型的数据,甚至是任何 Java 类。可以将 Session 看成是一个 Java 容器类。
  • 有效期
    • 使用 Cookie 可以保证长时间登录有效,只要设置 Cookie 的 maxAge 属性为一个很大的数字。
    • 而 Session 虽然理论上也可以通过设置很大的数值来保持长时间登录有效,但是,由于 Session 依赖于名为 JESSIONID 的 Cookie,而 Cookie JESSIONIDmaxAge 默认为-1,只要关闭了浏览器该 Session 就会失效,因此,Session 不能实现信息永久有效的效果。使用 URL 地址重写也不能实现。
  • 浏览器的支持
    • 浏览器如果禁用 Cookie,则 Cookie 不能使用。
    • 浏览器如果禁用 Cookie,需要使用 Session 以及 URL 地址重写。需要注意的是:所有的用到 Session 程序的 URL 都要使用response.encodeURL(StringURL)response.encodeRediretURL(String URL)进行 URL 地址重写,否则导致 Session 会话跟踪失效。
  • 跨域名
    • Cookie 支持跨域名。
    • Session 不支持跨域名。

参考资料

设计模式之原型模式

意图

原型模式(Prototype)是一种创建型设计模式, 使你能够复制已有对象, 而又无需使代码依赖它们所属的类。

原型模式主要用于对象的复制,它的核心是就是类图中的原型类 Prototype。Prototype 类需要具备以下两个条件:

  • 实现 Cloneable 接口。在 java 语言有一个 Cloneable 接口,它的作用只有一个,就是在运行时通知虚拟机可以安全地在实现了此接口的类上使用 clone 方法。在 java 虚拟机中,只有实现了这个接口的类才可以被拷贝,否则在运行时会抛出 CloneNotSupportedException 异常。
  • 重写 Object 类中的 clone 方法。Java 中,所有类的父类都是 Object 类,Object 类中有一个 clone 方法,作用是返回对象的一个拷贝,但是其作用域 protected 类型的,一般的类无法调用,因此,Prototype 类需要将 clone 方法的作用域修改为 public 类型。

浅拷贝与深拷贝

浅拷贝是指当对象的字段值被复制时,字段引用的对象不会被复制。

例如:如果一个对象有一个指向字符串的字段,并且我们对该对象做了一个浅拷贝,那麽两个对象将引用同一个字符串。

深拷贝是指当一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝。

适用场景

  • 如果你需要复制一些对象, 同时又希望代码独立于这些对象所属的具体类, 可以使用原型模式。
  • 如果子类的区别仅在于其对象的初始化方式, 那么你可以使用该模式来减少子类的数量。 别人创建这些子类的目的可能是为了创建特定类型的对象。

结构

img

  1. 原型 (Prototype) 接口将对克隆方法进行声明。 在绝大多数情况下, 其中只会有一个名为 clone克隆的方法。
  2. 具体原型 (Concrete Prototype) 类将实现克隆方法。 除了将原始对象的数据复制到克隆体中之外, 该方法有时还需处理克隆过程中的极端情况, 例如克隆关联对象和梳理递归依赖等等。
  3. 客户端 (Client) 可以复制实现了原型接口的任何对象。

伪代码

在本例中, 原型模式能让你生成完全相同的几何对象副本, 同时无需代码与对象所属类耦合。

img

所有形状类都遵循同一个提供克隆方法的接口。 在复制自身成员变量值到结果对象前, 子类可调用其父类的克隆方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 基础原型。
abstract class Shape is
field X: int
field Y: int
field color: string

// 常规构造函数。
constructor Shape() is
// ...

// 原型构造函数。使用已有对象的数值来初始化一个新对象。
constructor Shape(source: Shape) is
this()
this.X = source.X
this.Y = source.Y
this.color = source.color

// clone(克隆)操作会返回一个形状子类。
abstract method clone():Shape


// 具体原型。克隆方法会创建一个新对象并将其传递给构造函数。直到构造函数运
// 行完成前,它都拥有指向新克隆对象的引用。因此,任何人都无法访问未完全生
// 成的克隆对象。这可以保持克隆结果的一致。
class Rectangle extends Shape is
field width: int
field height: int

constructor Rectangle(source: Rectangle) is
// 需要调用父构造函数来复制父类中定义的私有成员变量。
super(source)
this.width = source.width
this.height = source.height

method clone():Shape is
return new Rectangle(this)


class Circle extends Shape is
field radius: int

constructor Circle(source: Circle) is
super(source)
this.radius = source.radius

method clone():Shape is
return new Circle(this)


// 客户端代码中的某个位置。
class Application is
field shapes: array of Shape

constructor Application() is
Circle circle = new Circle()
circle.X = 10
circle.Y = 10
circle.radius = 20
shapes.add(circle)

Circle anotherCircle = circle.clone()
shapes.add(anotherCircle)
// 变量 `anotherCircle(另一个圆)`与 `circle(圆)`对象的内
// 容完全一样。

Rectangle rectangle = new Rectangle()
rectangle.width = 10
rectangle.height = 20
shapes.add(rectangle)

method businessLogic() is
// 原型是很强大的东西,因为它能在不知晓对象类型的情况下生成一个与
// 其完全相同的复制品。
Array shapesCopy = new Array of Shapes.

// 例如,我们不知晓形状数组中元素的具体类型,只知道它们都是形状。
// 但在多态机制的帮助下,当我们在某个形状上调用 `clone(克隆)`
// 方法时,程序会检查其所属的类并调用其中所定义的克隆方法。这样,
// 我们将获得一个正确的复制品,而不是一组简单的形状对象。
foreach (s in shapes) do
shapesCopy.add(s.clone())

// `shapesCopy(形状副本)`数组中包含 `shape(形状)`数组所有
// 子元素的复制品。

案例

使用示例: Java 的 Cloneable (可克隆) 接口就是立即可用的原型模式。

任何类都可通过实现该接口来实现可被克隆的性质。

识别方法: 原型可以简单地通过 clonecopy等方法来识别。

与其他模式的关系

参考资料

设计模式之建造者模式

意图

建造者模式(Builder)是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。

使用建造者模式,用户就只需要指定需要建造的类型,具体的建造过程和细节并不需要知道。

建造者模式允许修改一个产品的内部表示。

它将构造和表示两块代码隔离开来。

它很好的控制了构建过程。

img

建造者模式流程说明:

  1. 客户端创建 Director 对象并配置它所需要的 Builder 对象。
  2. Director 负责通知 builder 何时建造 product 的部件。
  3. Builder 处理 director 的请求并添加 product 的部件。
  4. 客户端从 builder 处获得 product。

适用场景

  • 使用建造者模式可避免 “重叠构造函数 (telescopic constructor)” 的出现。
  • 当你希望使用代码创建不同形式的产品时, 可使用建造者模式。
  • 使用建造者构造组合树或其他复杂对象。

结构

img

  1. 建造者 (Builder) 接口声明在所有类型建造者中通用的产品构造步骤。
  2. 具体建造者 (Concrete Builders) 提供构造过程的不同实现。 具体建造者也可以构造不遵循通用接口的产品。
  3. 产品 (Products) 是最终生成的对象。 由不同建造者构造的产品无需属于同一类层次结构或接口。
  4. 主管 (Director) 类定义调用构造步骤的顺序, 这样你就可以创建和复用特定的产品配置。
  5. 客户端 (Client) 必须将某个建造者对象与主管类关联。 一般情况下, 你只需通过主管类构造函数的参数进行一次性关联即可。 此后主管类就能使用建造者对象完成后续所有的构造任务。 但在客户端将建造者对象传递给主管类制造方法时还有另一种方式。 在这种情况下, 你在使用主管类生产产品时每次都可以使用不同的建造者。

【Product】产品类,由多个部件构成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Product {
List<String> parts = new ArrayList<String>();

public void AddPart(String part) {
parts.add(part);
}

public void show() {
System.out.println("============== 产品创建 ==============");
for (String part : parts) {
System.out.println(part);
}
}
}

【Builder】

抽象建造者,确定产品由 ABC 三个部件构成,并声明一个得到产品建造后结果的方法 getResult。

1
2
3
4
5
6
interface Builder {
public void buildPartA();
public void buildPartB();
public void buildPartC();
public Product getResult();
}

【ConcreteBuilder】

实现 Builder 接口中的具体方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ConcreteBuilder implements Builder {
private Product product = new Product();

@Override
public void buildPartA() {
product.AddPart("部件A");
}

@Override
public void buildPartB() {
product.AddPart("部件B");
}

@Override
public void buildPartC() {
product.AddPart("部件C");
}

@Override
public Product getResult() {
return product;
}
}

【Director】

指挥者类,指挥建造 Product 的过程(控制构建各部分组件的顺序)。

1
2
3
4
5
6
7
class Director {
public void construct(Builder builder) {
builder.buildPartC();
builder.buildPartA();
builder.buildPartB();
}
}

【客户端】

用户并不需要知道具体的建造过程,只需指定建造 Product 具体类型。

1
2
3
4
5
6
7
8
9
10
public class BuilderPattern {
public static void main(String[] args) {
Director director = new Director();
Builder builder = new ConcreteBuilder();

director.construct(builder);
Product product = builder.getResult();
product.show();
}
}

【输出】

1
2
3
4
============== 产品创建 ==============
部件C
部件A
部件B

伪代码

下面关于建造者模式的例子演示了你可以如何复用相同的对象构造代码来生成不同类型的产品——例如汽车 (Car)——及其相应的使用手册 (Manual)。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// 只有当产品较为复杂且需要详细配置时,使用建造者模式才有意义。下面的两个
// 产品尽管没有同样的接口,但却相互关联。
class Car is
// 一辆汽车可能配备有 GPS 设备、行车电脑和几个座位。不同型号的汽车(
// 运动型轿车、SUV 和敞篷车)可能会安装或启用不同的功能。

class Manual is
// 用户使用手册应该根据汽车配置进行编制,并介绍汽车的所有功能。


// 建造者接口声明了创建产品对象不同部件的方法。
interface Builder is
method reset()
method setSeats(...)
method setEngine(...)
method setTripComputer(...)
method setGPS(...)

// 具体建造者类将遵循建造者接口并提供生成步骤的具体实现。你的程序中可能会
// 有多个以不同方式实现的建造者变体。
class CarBuilder implements Builder is
private field car:Car

// 一个新的建造者实例必须包含一个在后续组装过程中使用的空产品对象。
constructor CarBuilder() is
this.reset()

// reset(重置)方法可清除正在生成的对象。
method reset() is
this.car = new Car()

// 所有生成步骤都会与同一个产品实例进行交互。
method setSeats(...) is
// 设置汽车座位的数量。

method setEngine(...) is
// 安装指定的引擎。

method setTripComputer(...) is
// 安装行车电脑。

method setGPS(...) is
// 安装全球定位系统。

// 具体建造者需要自行提供获取结果的方法。这是因为不同类型的建造者可能
// 会创建不遵循相同接口的、完全不同的产品。所以也就无法在建造者接口中
// 声明这些方法(至少在静态类型的编程语言中是这样的)。
//
// 通常在建造者实例将结果返回给客户端后,它们应该做好生成另一个产品的
// 准备。因此建造者实例通常会在 `getProduct(获取产品)`方法主体末尾
// 调用重置方法。但是该行为并不是必需的,你也可让建造者等待客户端明确
// 调用重置方法后再去处理之前的结果。
method getProduct():Car is
product = this.car
this.reset()
return product

// 建造者与其他创建型模式的不同之处在于:它让你能创建不遵循相同接口的产品。
class CarManualBuilder implements Builder is
private field manual:Manual

constructor CarManualBuilder() is
this.reset()

method reset() is
this.manual = new Manual()

method setSeats(...) is
// 添加关于汽车座椅功能的文档。

method setEngine(...) is
// 添加关于引擎的介绍。

method setTripComputer(...) is
// 添加关于行车电脑的介绍。

method setGPS(...) is
// 添加关于 GPS 的介绍。

method getProduct():Manual is
// 返回使用手册并重置建造者。


// 主管只负责按照特定顺序执行生成步骤。其在根据特定步骤或配置来生成产品时
// 会很有帮助。由于客户端可以直接控制建造者,所以严格意义上来说,主管类并
// 不是必需的。
class Director is
private field builder:Builder

// 主管可同由客户端代码传递给自身的任何建造者实例进行交互。客户端可通
// 过这种方式改变最新组装完毕的产品的最终类型。
method setBuilder(builder:Builder)
this.builder = builder

// 主管可使用同样的生成步骤创建多个产品变体。
method constructSportsCar(builder: Builder) is
builder.reset()
builder.setSeats(2)
builder.setEngine(new SportEngine())
builder.setTripComputer(true)
builder.setGPS(true)

method constructSUV(builder: Builder) is
// ...


// 客户端代码会创建建造者对象并将其传递给主管,然后执行构造过程。最终结果
// 将需要从建造者对象中获取。
class Application is

method makeCar() is
director = new Director()

CarBuilder builder = new CarBuilder()
director.constructSportsCar(builder)
Car car = builder.getProduct()

CarManualBuilder builder = new CarManualBuilder()
director.constructSportsCar(builder)

// 最终产品通常需要从建造者对象中获取,因为主管不知晓具体建造者和
// 产品的存在,也不会对其产生依赖。
Manual manual = builder.getProduct()

案例

使用示例: 建造者模式是 Java 世界中的一个著名模式。 当你需要创建一个可能有许多配置选项的对象时, 该模式会特别有用。

建造者在 Java 核心程序库中得到了广泛的应用:

识别方法: 建造者模式可以通过类来识别, 它拥有一个构建方法和多个配置结果对象的方法。 建造者方法通常支持方法链 (例如 someBuilder->setValueA(1)->setValueB(2)->create()

与其他模式的关系

参考资料

设计模式之抽象工厂模式

意图

抽象工厂模式 (Abstract Factory)是一种创建型设计模式, 它能创建一系列相关的对象, 而无需指定其具体类。

**优点 **

  • 抽象工厂模式隔离了具体类的生成,用户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂变得相对容易。所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需要改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。另外,应用抽象工厂模式可以实现高内聚低耦合的设计目的,因此抽象工厂模式得到了广泛的应用。

  • 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。

  • 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开放封闭原则”

缺点

  • 在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改,显然会带来较大的不便。

适用场景

抽象工厂模式适用场景:

一个系统要独立于它的产品的创建、组合和表示时。

一个系统要由多个产品系列中的一个来配置时。

当你要强调一系列相关的产品对象的设计以便进行联合使用时。

当你提供一个产品类库,而只想显示它们的接口而不是实现时。

结构

img

结构说明

  1. 抽象产品 (Abstract Product) 为构成系列产品的一组不同但相关的产品声明接口。
  2. 具体产品 (Concrete Product) 是抽象产品的多种不同类型实现。 所有变体 (维多利亚/现代) 都必须实现相应的抽象产品 (椅子/沙发)。
  3. 抽象工厂 (Abstract Factory) 接口声明了一组创建各种抽象产品的方法。
  4. 具体工厂 (Concrete Factory) 实现抽象工厂的构建方法。 每个具体工厂都对应特定产品变体, 且仅创建此种产品变体。
  5. 尽管具体工厂会对具体产品进行初始化, 其构建方法签名必须返回相应的抽象产品。 这样, 使用工厂类的客户端代码就不会与工厂创建的特定产品变体耦合。 客户端 (Client) 只需通过抽象接口调用工厂和产品对象, 就能与任何具体工厂/产品变体交互。

结构代码范式

【AbstractProduct】

声明一个接口,这个接口中包含产品对象类型。

1
2
3
4
5
6
7
abstract class AbstractProductA {
public abstract void show();
}

abstract class AbstractProductB {
public abstract void show();
}

【ConcreteProduct】

定义一个产品对象,这个产品对象是由相关的具体工厂创建的。

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
class ConcreteProductA1 extends AbstractProductA {
    @Override
    public void show() {
        System.out.println("ConcreteProductA1");
    }
}

class ConcreteProductA2 extends AbstractProductA {
    @Override
    public void show() {
        System.out.println("ConcreteProductA2");
    }
}

class ConcreteProductB1 extends AbstractProductB {
    @Override
    public void show() {
        System.out.println("ConcreteProductB1");
    }
}

class ConcreteProductB2 extends AbstractProductB {
    @Override
    public void show() {
        System.out.println("ConcreteProductB2");
    }
}

【AbstractFactory】

声明一个接口,这个接口中包含创建抽象产品对象的方法。

1
2
3
4
abstract class AbstractFactory {
public abstract AbstractProductA createProductA();
public abstract AbstractProductB createProductB();
}

【ConcreteFactory】

实现创建具体产品对象的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ConcreteFactory1 extends AbstractFactory {
@Override
public AbstractProductA createProductA() {
return new ConcreteProductA1();
}

@Override
public AbstractProductB createProductB() {
return new ConcreteProductB1();
}
}

class ConcreteFactory2 extends AbstractFactory {
@Override
public AbstractProductA createProductA() {
return new ConcreteProductA2();
}

@Override
public AbstractProductB createProductB() {
return new ConcreteProductB2();
}
}

【客户端】

只使用 AbstractFactoryAbstractProduct 声明的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AbstarctFactoryPattern {
public static void main(String[] args) {
AbstractFactory factory1 = new ConcreteFactory1();
AbstractProductA productA1 = factory1.createProductA();
AbstractProductB productB1 = factory1.createProductB();
productA1.show();
productB1.show();

AbstractFactory factory2 = new ConcreteFactory2();
AbstractProductA productA2 = factory2.createProductA();
AbstractProductB productB2 = factory2.createProductB();
productA2.show();
productB2.show();
}
}

【输出】

1
2
3
4
ConcreteProductA1
ConcreteProductB1
ConcreteProductA2
ConcreteProductB2

伪代码

下面例子通过应用抽象工厂模式, 使得客户端代码无需与具体 UI 类耦合, 就能创建跨平台的 UI 元素, 同时确保所创建的元素与指定的操作系统匹配。

img

跨平台应用中的相同 UI 元素功能类似, 但是在不同操作系统下的外观有一定差异。 此外, 你需要确保 UI 元素与当前操作系统风格一致。 你一定不希望在 Windows 系统下运行的应用程序中显示 macOS 的控件。

抽象工厂接口声明一系列构建方法, 客户端代码可调用它们生成不同风格的 UI 元素。 每个具体工厂对应特定操作系统, 并负责生成符合该操作系统风格的 UI 元素。

其运作方式如下: 应用程序启动后检测当前操作系统。 根据该信息, 应用程序通过与该操作系统对应的类创建工厂对象。 其余代码使用该工厂对象创建 UI 元素。 这样可以避免生成错误类型的元素。

使用这种方法, 客户端代码只需调用抽象接口, 而无需了解具体工厂类和 UI 元素。 此外, 客户端代码还支持未来添加新的工厂或 UI 元素。

这样一来, 每次在应用程序中添加新的 UI 元素变体时, 你都无需修改客户端代码。 你只需创建一个能够生成这些 UI 元素的工厂类, 然后稍微修改应用程序的初始代码, 使其能够选择合适的工厂类即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 抽象工厂接口声明了一组能返回不同抽象产品的方法。这些产品属于同一个系列
// 且在高层主题或概念上具有相关性。同系列的产品通常能相互搭配使用。系列产
// 品可有多个变体,但不同变体的产品不能搭配使用。
interface GUIFactory is
method createButton():Button
method createCheckbox():Checkbox


// 具体工厂可生成属于同一变体的系列产品。工厂会确保其创建的产品能相互搭配
// 使用。具体工厂方法签名会返回一个抽象产品,但在方法内部则会对具体产品进
// 行实例化。
class WinFactory implements GUIFactory is
method createButton():Button is
return new WinButton()
method createCheckbox():Checkbox is
return new WinCheckbox()

// 每个具体工厂中都会包含一个相应的产品变体。
class MacFactory implements GUIFactory is
method createButton():Button is
return new MacButton()
method createCheckbox():Checkbox is
return new MacCheckbox()


// 系列产品中的特定产品必须有一个基础接口。所有产品变体都必须实现这个接口。
interface Button is
method paint()

// 具体产品由相应的具体工厂创建。
class WinButton implements Button is
method paint() is
// 根据 Windows 样式渲染按钮。

class MacButton implements Button is
method paint() is
// 根据 macOS 样式渲染按钮

// 这是另一个产品的基础接口。所有产品都可以互动,但是只有相同具体变体的产
// 品之间才能够正确地进行交互。
interface Checkbox is
method paint()

class WinCheckbox implements Checkbox is
method paint() is
// 根据 Windows 样式渲染复选框。

class MacCheckbox implements Checkbox is
method paint() is
// 根据 macOS 样式渲染复选框。

// 客户端代码仅通过抽象类型(GUIFactory、Button 和 Checkbox)使用工厂
// 和产品。这让你无需修改任何工厂或产品子类就能将其传递给客户端代码。
class Application is
private field factory: GUIFactory
private field button: Button
constructor Application(factory: GUIFactory) is
this.factory = factory
method createUI() is
this.button = factory.createButton()
method paint() is
button.paint()


// 程序会根据当前配置或环境设定选择工厂类型,并在运行时创建工厂(通常在初
// 始化阶段)。
class ApplicationConfigurator is
method main() is
config = readApplicationConfigFile()

if (config.OS == "Windows") then
factory = new WinFactory()
else if (config.OS == "Mac") then
factory = new MacFactory()
else
throw new Exception("错误!未知的操作系统。")

Application app = new Application(factory)

案例

众所周知,苹果和三星这两家世界级的电子产品厂商都生产手机和电脑。

我们以生产手机和电脑为例,演示一下抽象工厂模式的应用

【AbstractProduct 角色】

首先,定义手机和电脑两个抽象接口,他们都有各自的产品信息。

1
2
3
4
5
6
7
interface Telephone {
public String getProductInfo();
}

interface Computer {
public String getProductInfo();
}

【ConcreteProduct 角色】

ConcreteProduct 根据 AbstractProduct 来定义具体的产品属性、方法。

在我们的例子中,苹果、三星两家公司的手机和电脑都有各自的具体产品信息。

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
class AppleTelephone implements Telephone {

@Override
public String getProductInfo() {
return "苹果手机,采用ios系统";
}
}

class SamsungTelephone implements Telephone {

@Override
public String getProductInfo() {
return "三星手机,采用android系统";
}
}

class AppleComputer implements Computer {

@Override
public String getProductInfo() {
return "苹果电脑,采用mac系统";
}
}

class SamsungComputer implements Computer {

@Override
public String getProductInfo() {
return "三星电脑,采用windows系统";
}
}

【AbstractFactory 角色】

苹果,三星这两个厂商都生产手机和电脑。所以它们可以有一个抽象父类或父接口,提供生产手机和生产电脑的方法。

1
2
3
4
5
6
interface ElectronicFactory {

public Telephone produceTelephone();

public Computer produceComputer();
}

【ConcreteFactory 角色】

苹果、三星工厂分别实现父接口,生产不同类型的产品。

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
class AppleFactory implements ElectronicFactory {

@Override
public Telephone produceTelephone() {
return new AppleTelephone();
}

@Override
public Computer produceComputer() {
return new AppleComputer();
}
}

class SamsungFactory implements ElectronicFactory {

@Override
public Telephone produceTelephone() {
return new SamsungTelephone();
}

@Override
public Computer produceComputer() {
return new SamsungComputer();
}
}

【客户端】

1
2
3
4
5
6
7
8
9
public class PhoneFactoryDemo {
public static void main(String[] args) {
ElectronicFactory appleFactory = new AppleFactory();
Telephone phone = appleFactory.produceTelephone();
System.out.println(phone.getProductInfo());
Computer computer = appleFactory.produceComputer();
System.out.println(computer.getProductInfo());
}
}

【输出】

1
2
苹果手机,采用ios系统
苹果电脑,采用mac系统

与其他模式的关系

参考资料

设计模式之工厂方法模式

意图

工厂方法模式 (Factory Method)是一种创建型设计模式, 其在父类中提供一个创建对象的方法, 让子类决定实例化对象的类型。

  • 工厂模式中,增加一种产品类,就要增加一个工厂类:因为每个工厂类只能创建一种产品的实例。
  • 工厂模式遵循“开放-封闭原则”:工厂模式中,新增一种产品并不需要修改原有类,仅仅是扩展。

简单工厂模式相比于工厂方法模式

优点:工厂类中包含必要的逻辑判断,可根据客户端的选择条件动态实例化需要的类。对于客户端来说,去除了对具体产品的依赖。

缺点违背了开放封闭原则。 每添加一个新的产品,都需要对原有类进行修改。增加维护成本,且不易于维护。

开放封闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

适用场景

  • 当你在编写代码的过程中, 如果无法预知对象确切类别及其依赖关系时, 可使用工厂方法。
  • 如果你希望用户能扩展你软件库或框架的内部组件, 可使用工厂方法。
  • 如果你希望复用现有对象来节省系统资源, 而不是每次都重新创建对象, 可使用工厂方法。

结构

img

结构说明

  1. 产品 (Product) 将会对接口进行声明。 对于所有由创建者及其子类构建的对象, 这些接口都是通用的。
  2. 具体产品 (Concrete Products) 是产品接口的不同实现。
  3. 创建者 (Creator) 类声明返回产品对象的工厂方法。 该方法的返回对象类型必须与产品接口相匹配。
  • 你可以将工厂方法声明为抽象方法, 强制要求每个子类以不同方式实现该方法。 或者, 你也可以在基础工厂方法中返回默认产品类型。
  • 注意, 尽管它的名字是创建者, 但他最主要的职责并不是创建产品。 一般来说, 创建者类包含一些与产品相关的核心业务逻辑。 工厂方法将这些逻辑处理从具体产品类中分离出来。 打个比方, 大型软件开发公司拥有程序员培训部门。 但是, 这些公司的主要工作还是编写代码, 而非生产程序员。
  1. 具体创建者 (Concrete Creators) 将会重写基础工厂方法, 使其返回不同类型的产品。
    注意, 并不一定每次调用工厂方法都会创建新的实例。 工厂方法也可以返回缓存、 对象池或其他来源的已有对象。

结构代码范式

【Product】

定义产品对象的接口。

1
2
3
abstract class Product {
public abstract void use();
}

【ConcreteProduct】

实现 Product 接口。

1
2
3
4
5
6
7
8
9
10
class ConcreteProduct extends Product {
public ConcreteProduct() {
System.out.println("创建 ConcreteProduct 产品");
}

@Override
public void Use() {
System.out.println("使用 ConcreteProduct 产品");
}
}

【Creator】

声明工厂方法,它会返回一个产品类型的对象Creator 也可以实现一个默认的工厂方法 factoryMethod() ,以返回一个默认的具体产品类型。

1
2
3
interface Creator {
public Product factoryMethod();
}

【ConcreteCreator】

覆写 Creator 中的工厂方法 factoryMethod()

1
2
3
4
5
6
class ConcreteCreator implements Creator {
@Override
public Product factoryMethod() {
return new ConcreteProduct();
}
}

【客户端】

1
2
3
4
5
6
7
public class factoryMethodPattern {
public static void main(String[] args) {
Creator factory = new ConcreteCreator();
Product product = factory.factoryMethod();
product.Use();
}
}

【输出】

1
2
创建 ConcreteProduct 产品
使用 ConcreteProduct 产品

伪代码

以下示例演示了如何使用工厂方法开发跨平台 UI (用户界面) 组件, 并同时避免客户代码与具体 UI 类之间的耦合。

img

基础对话框类使用不同的 UI 组件渲染窗口。 在不同的操作系统下, 这些组件外观或许略有不同, 但其功能保持一致。 Windows 系统中的按钮在 Linux 系统中仍然是按钮。

如果使用工厂方法, 就不需要为每种操作系统重写对话框逻辑。 如果我们声明了一个在基本对话框类中生成按钮的工厂方法, 那么我们就可以创建一个对话框子类, 并使其通过工厂方法返回 Windows 样式按钮。 子类将继承对话框基础类的大部分代码, 同时在屏幕上根据 Windows 样式渲染按钮。

如需该模式正常工作, 基础对话框类必须使用抽象按钮 (例如基类或接口), 以便将其扩展为具体按钮。 这样一来, 无论对话框中使用何种类型的按钮, 其代码都可以正常工作。

你可以使用此方法开发其他 UI 组件。 不过, 每向对话框中添加一个新的工厂方法, 你就离抽象工厂模式更近一步。 我们将在稍后谈到这个模式。

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
// 创建者类声明的工厂方法必须返回一个产品类的对象。创建者的子类通常会提供
// 该方法的实现。
class Dialog is
// 创建者还可提供一些工厂方法的默认实现。
abstract method createButton():Button

// 请注意,创建者的主要职责并非是创建产品。其中通常会包含一些核心业务
// 逻辑,这些逻辑依赖于由工厂方法返回的产品对象。子类可通过重写工厂方
// 法并使其返回不同类型的产品来间接修改业务逻辑。
method render() is
// 调用工厂方法创建一个产品对象。
Button okButton = createButton()
// 现在使用产品。
okButton.onClick(closeDialog)
okButton.render()


// 具体创建者将重写工厂方法以改变其所返回的产品类型。
class WindowsDialog extends Dialog is
method createButton():Button is
return new WindowsButton()

class WebDialog extends Dialog is
method createButton():Button is
return new HTMLButton()


// 产品接口中将声明所有具体产品都必须实现的操作。
interface Button is
method render()
method onClick(f)

// 具体产品需提供产品接口的各种实现。
class WindowsButton implements Button is
method render(a, b) is
// 根据 Windows 样式渲染按钮。
method onClick(f) is
// 绑定本地操作系统点击事件。

class HTMLButton implements Button is
method render(a, b) is
// 返回一个按钮的 HTML 表述。
method onClick(f) is
// 绑定网络浏览器的点击事件。


class Application is
field dialog: Dialog

// 程序根据当前配置或环境设定选择创建者的类型。
method initialize() is
config = readApplicationConfigFile()

if (config.OS == "Windows") then
dialog = new WindowsDialog()
else if (config.OS == "Web") then
dialog = new WebDialog()
else
throw new Exception("错误!未知的操作系统。")

// 当前客户端代码会与具体创建者的实例进行交互,但是必须通过其基本接口
// 进行。只要客户端通过基本接口与创建者进行交互,你就可将任何创建者子
// 类传递给客户端。
method main() is
this.initialize()
dialog.render()

案例

使用示例: 工厂方法模式在 Java 代码中得到了广泛使用。 当你需要在代码中提供高层次的灵活性时, 该模式会非常实用。

核心 Java 程序库中有该模式的应用:

识别方法: 工厂方法可通过构建方法来识别, 它会创建具体类的对象, 但以抽象类型或接口的形式返回这些对象。

还是以 简单工厂模式 里的例子来进行说明。

如何实现一个具有加减乘除基本功能的计算器?

两种模式的 ProductConcreteProduct 角色代码没有区别,不再赘述。

差异在于 Factory 角色部分,以及客户端部分,请在代码中体会。

【Creator 角色】

1
2
3
4
// Creator 角色,定义返回产品实例的公共工厂方法
interface OperationFactory {
public Operation factoryMethod();
}

【ConcreteCreator 角色】

和简单工厂模式相比,每一种产品都会有一个具体的工厂类负责生产实例。

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
// ConcreteCreator 角色,具体实现 Creator 中的方法
class AddFactory implements OperationFactory {
@Override
public Operation factoryMethod() {
return new Add();
}
}

// ConcreteCreator 角色,具体实现 Creator 中的方法
class SubFactory implements OperationFactory {
@Override
public Operation factoryMethod() {
return new Sub();
}
}

// ConcreteCreator 角色,具体实现 Creator 中的方法
class MulFactory implements OperationFactory {
@Override
public Operation factoryMethod() {
return new Mul();
}
}

// ConcreteCreator 角色,具体实现 Creator 中的方法
class DivFactory implements OperationFactory {
@Override
public Operation factoryMethod() {
return new Div();
}
}

【Client 角色】

与简单工厂模式中无需关注具体创建不同,工厂模式中需要指定具体工厂,以负责生产具体对应的产品。

1
2
3
4
5
6
7
8
9
10
11
// Client 角色,需要指定具体工厂,以负责生产具体产品
public class factoryMethodPattern {
public static void main(String[] args) {
OperationFactory factory = new SubFactory();
Operation oper = factory.factoryMethod();
oper.numA = 3;
oper.numB = 2;
double result = oper.getResult();
System.out.println("result = " + result);
}
}

与其他模式的关系

参考资料