0%

POM文件简介

pom 全称:Project Object Model,即项目对象模型。

Maven 把一个项目的结构和内容抽象成一个模型,在 pom.xml 文件中进行声明和描述。所以说 pom.xml 是 Maven 的核心。

pom文件定于了一个maven项目的maven配置,一般pom文件的放在项目或者模块的根目录下。

maven的遵循约定大于配置,约定了如下的目录结构:

目录 目的
${basedir} 存放pom.xml和所有的子目录
${basedir}/src/main/java 项目的java源代码
${basedir}/src/main/resources 项目的资源,比如说property文件,springmvc.xml
${basedir}/src/test/java 项目的测试类,比如说Junit代码
${basedir}/src/test/resources 测试用的资源
${basedir}/src/main/scripts 项目脚本源码的目录
${basedir}/src/main/webapp/WEB-INF web应用文件目录,web项目的信息,比如存放web.xml、本地图片、jsp视图页面
${basedir}/target 打包输出目录
${basedir}/target/classes 编译输出目录
${basedir}/target/site 生成文档的目录,可以通过index.html查看项目的文档
${basedir}/target/test-classes 测试编译输出目录
Test.java Maven只会自动运行符合该命名规则的测试类
~/.m2/repository Maven默认的本地仓库目录位置
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
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<!-- The Basics -->
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<packaging>...</packaging>
<dependencies>...</dependencies>
<parent>...</parent>
<dependencyManagement>...</dependencyManagement>
<modules>...</modules>
<properties>...</properties>

<!-- Build Settings -->
<build>...</build>
<reporting>...</reporting>

<!-- More Project Information -->
<name>...</name>
<description>...</description>
<url>...</url>
<inceptionYear>...</inceptionYear>
<licenses>...</licenses>
<organization>...</organization>
<developers>...</developers>
<contributors>...</contributors>

<!-- Environment Settings -->
<issueManagement>...</issueManagement>
<ciManagement>...</ciManagement>
<mailingLists>...</mailingLists>
<scm>...</scm>
<prerequisites>...</prerequisites>
<repositories>...</repositories>
<pluginRepositories>...</pluginRepositories>
<distributionManagement>...</distributionManagement>
<profiles>...</profiles>
</project>

完整的 pom.xml 文件:

请注意,modelVersion 为 4.0.0 这是目前唯一支持Maven 2和3的POM版本,且是必填项

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
<project xmlns="http://maven.apache.org/POM/4.0.0" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">

<!--声明项目描述符遵循哪一个POM模型版本。模型本身的版本很少改变,虽然如此,但它仍然是必不可少的,这是为了当Maven引入了新的特性或者其他模型变更的时候,确保稳定性。 -->
<modelVersion>4.0.0</modelVersion>

<!-------------------------------------1、基础配置---------------------------------------------->

<!--项目的全球唯一标识符,通常使用全限定的包名区分该项目和其他项目。并且构建时生成的路径也是由此生成, 如com.mycompany.app生成的相对路径为:/com/mycompany/app -->
<groupId>asia.banseon</groupId>
<!-- 构件的标识符,它和group ID一起唯一标识一个构件。换句话说,你不能有两个不同的项目拥有同样的artifact ID和groupID;在某个
特定的group ID下,artifact ID也必须是唯一的。构件是项目产生的或使用的一个东西,Maven为项目产生的构件包括:JARs,源 码,二进制发布和WARs等。 -->
<artifactId>banseon-maven2</artifactId>
<!--项目产生的构件类型,例如jar、war、ear、pom。插件可以创建他们自己的构件类型,所以前面列的不是全部构件类型 -->
<packaging>jar</packaging>
<!--项目当前版本,格式为:主版本.次版本.增量版本-限定版本号 -->
<version>1.0-SNAPSHOT</version>
<!--项目的名称, Maven产生的文档用 -->
<name>banseon-maven</name>
<!--项目主页的URL, Maven产生的文档用 -->
<url>http://www.baidu.com/banseon</url>
<!-- 项目的详细描述, Maven 产生的文档用。 当这个元素能够用HTML格式描述时(例如,CDATA中的文本会被解析器忽略,就可以包含HTML标
签), 不鼓励使用纯文本描述。如果你需要修改产生的web站点的索引页面,你应该修改你自己的索引页文件,而不是调整这里的文档。 -->
<description>A maven project to study maven.</description>

<!---------------------------------------------1----------------------------------------------->

<!--父项目的坐标。如果项目中没有规定某个元素的值,那么父项目中的对应值即为项目的默认值。 坐标包括group ID,artifact ID和version。 -->
<parent>
<!--被继承的父项目的构件标识符 -->
<artifactId>...</artifactId>
<!--被继承的父项目的全球唯一标识符 -->
<groupId>...</groupId>
<!--被继承的父项目的版本 -->
<version>...</version>
<!-- 父项目的pom.xml文件的相对路径。相对路径允许你选择一个不同的路径。默认值是../pom.xml。Maven首先在构建当前项目的地方寻找父项
目的pom,其次在文件系统的这个位置(relativePath位置),然后在本地仓库,最后在远程仓库寻找父项目的pom。 -->
<relativePath>···</relativePath>
</parent>


<!--描述了这个项目构建环境中的前提条件。 -->
<prerequisites>
<!--构建该项目或使用该插件所需要的Maven的最低版本 -->
<maven />
</prerequisites>
<!--项目的问题管理系统(Bugzilla, Jira, Scarab,或任何你喜欢的问题管理系统)的名称和URL,本例为 jira -->
<issueManagement>
<!--问题管理系统(例如jira)的名字, -->
<system>jira</system>
<!--该项目使用的问题管理系统的URL -->
<url>http://jira.baidu.com/banseon</url>
</issueManagement>
<!--项目持续集成信息 -->
<ciManagement>
<!--持续集成系统的名字,例如continuum -->
<system />
<!--该项目使用的持续集成系统的URL(如果持续集成系统有web接口的话)。 -->
<url />
<!--构建完成时,需要通知的开发者/用户的配置项。包括被通知者信息和通知条件(错误,失败,成功,警告) -->
<notifiers>
<!--配置一种方式,当构建中断时,以该方式通知用户/开发者 -->
<notifier>
<!--传送通知的途径 -->
<type />
<!--发生错误时是否通知 -->
<sendOnError />
<!--构建失败时是否通知 -->
<sendOnFailure />
<!--构建成功时是否通知 -->
<sendOnSuccess />
<!--发生警告时是否通知 -->
<sendOnWarning />
<!--不赞成使用。通知发送到哪里 -->
<address />
<!--扩展配置项 -->
<configuration />
</notifier>
</notifiers>
</ciManagement>
<!--项目创建年份,4位数字。当产生版权信息时需要使用这个值。 -->
<inceptionYear />
<!--项目相关邮件列表信息 -->
<mailingLists>
<!--该元素描述了项目相关的所有邮件列表。自动产生的网站引用这些信息。 -->
<mailingList>
<!--邮件的名称 -->
<name>Demo</name>
<!--发送邮件的地址或链接,如果是邮件地址,创建文档时,mailto: 链接会被自动创建 -->
<post>banseon@126.com</post>
<!--订阅邮件的地址或链接,如果是邮件地址,创建文档时,mailto: 链接会被自动创建 -->
<subscribe>banseon@126.com</subscribe>
<!--取消订阅邮件的地址或链接,如果是邮件地址,创建文档时,mailto: 链接会被自动创建 -->
<unsubscribe>banseon@126.com</unsubscribe>
<!--你可以浏览邮件信息的URL -->
<archive>http:/hi.baidu.com/banseon/demo/dev/</archive>
</mailingList>
</mailingLists>
<!--项目开发者列表 -->
<developers>
<!--某个项目开发者的信息 -->
<developer>
<!--SCM里项目开发者的唯一标识符 -->
<id>HELLO WORLD</id>
<!--项目开发者的全名 -->
<name>banseon</name>
<!--项目开发者的email -->
<email>banseon@126.com</email>
<!--项目开发者的主页的URL -->
<url />
<!--项目开发者在项目中扮演的角色,角色元素描述了各种角色 -->
<roles>
<role>Project Manager</role>
<role>Architect</role>
</roles>
<!--项目开发者所属组织 -->
<organization>demo</organization>
<!--项目开发者所属组织的URL -->
<organizationUrl>http://hi.baidu.com/banseon</organizationUrl>
<!--项目开发者属性,如即时消息如何处理等 -->
<properties>
<dept>No</dept>
</properties>
<!--项目开发者所在时区, -11到12范围内的整数。 -->
<timezone>-5</timezone>
</developer>
</developers>
<!--项目的其他贡献者列表 -->
<contributors>
<!--项目的其他贡献者。参见developers/developer元素 -->
<contributor>
<name />
<email />
<url />
<organization />
<organizationUrl />
<roles />
<timezone />
<properties />
</contributor>
</contributors>

<!--该元素描述了项目所有License列表。 应该只列出该项目的license列表,不要列出依赖项目的 license列表。如果列出多个license,用户可以选择它们中的一个而不是接受所有license。 -->
<licenses>
<!--描述了项目的license,用于生成项目的web站点的license页面,其他一些报表和validation也会用到该元素。 -->
<license>
<!--license用于法律上的名称 -->
<name>Apache 2</name>
<!--官方的license正文页面的URL -->
<url>http://www.baidu.com/banseon/LICENSE-2.0.txt</url>
<!--项目分发的主要方式: repo,可以从Maven库下载 manual, 用户必须手动下载和安装依赖 -->
<distribution>repo</distribution>
<!--关于license的补充信息 -->
<comments>A business-friendly OSS license</comments>
</license>
</licenses>

<!--SCM(Source Control Management)标签允许你配置你的代码库,供Maven web站点和其它插件使用。 -->
<scm>
<!--SCM的URL,该URL描述了版本库和如何连接到版本库。欲知详情,请看SCMs提供的URL格式和列表。该连接只读。 -->
<connection>
scm:svn:http://svn.baidu.com/banseon/maven/banseon/banseon-maven2-trunk(dao-trunk)
</connection>
<!--给开发者使用的,类似connection元素。即该连接不仅仅只读 -->
<developerConnection>
scm:svn:http://svn.baidu.com/banseon/maven/banseon/dao-trunk
</developerConnection>
<!--当前代码的标签,在开发阶段默认为HEAD -->
<tag />
<!--指向项目的可浏览SCM库(例如ViewVC或者Fisheye)的URL。 -->
<url>http://svn.baidu.com/banseon</url>
</scm>

<!--描述项目所属组织的各种属性。Maven产生的文档用 -->
<organization>
<!--组织的全名 -->
<name>demo</name>
<!--组织主页的URL -->
<url>http://www.baidu.com/banseon</url>
</organization>

<!--构建项目需要的信息 -->
<build>
<!--该元素设置了项目源码目录,当构建项目的时候,构建系统会编译目录里的源码。该路径是相对于pom.xml的相对路径。 -->
<sourceDirectory />
<!--该元素设置了项目脚本源码目录,该目录和源码目录不同:绝大多数情况下,该目录下的内容 会被拷贝到输出目录(因为脚本是被解释的,而不是被编译的)。 -->
<scriptSourceDirectory />
<!--该元素设置了项目单元测试使用的源码目录,当测试项目的时候,构建系统会编译目录里的源码。该路径是相对于pom.xml的相对路径。 -->
<testSourceDirectory />
<!--被编译过的应用程序class文件存放的目录。 -->
<outputDirectory />
<!--被编译过的测试class文件存放的目录。 -->
<testOutputDirectory />
<!--使用来自该项目的一系列构建扩展 -->
<extensions>
<!--描述使用到的构建扩展。 -->
<extension>
<!--构建扩展的groupId -->
<groupId />
<!--构建扩展的artifactId -->
<artifactId />
<!--构建扩展的版本 -->
<version />
</extension>
</extensions>
<!--当项目没有规定目标(Maven2 叫做阶段)时的默认值 -->
<defaultGoal />
<!--这个元素描述了项目相关的所有资源路径列表,例如和项目相关的属性文件,这些资源被包含在最终的打包文件里。 -->
<resources>
<!--这个元素描述了项目相关或测试相关的所有资源路径 -->
<resource>
<!-- 描述了资源的目标路径。该路径相对target/classes目录(例如${project.build.outputDirectory})。举个例
子,如果你想资源在特定的包里(org.apache.maven.messages),你就必须该元素设置为org/apache/maven /messages。然而,如果你只是想把资源放到源码目录结构里,就不需要该配置。 -->
<targetPath />
<!--是否使用参数值代替参数名。参数值取自properties元素或者文件里配置的属性,文件在filters元素里列出。 -->
<filtering />
<!--描述存放资源的目录,该路径相对POM路径 -->
<directory />
<!--包含的模式列表,例如**/*.xml. -->
<includes />
<!--排除的模式列表,例如**/*.xml -->
<excludes />
</resource>
</resources>
<!--这个元素描述了单元测试相关的所有资源路径,例如和单元测试相关的属性文件。 -->
<testResources>
<!--这个元素描述了测试相关的所有资源路径,参见build/resources/resource元素的说明 -->
<testResource>
<targetPath />
<filtering />
<directory />
<includes />
<excludes />
</testResource>
</testResources>
<!--构建产生的所有文件存放的目录 -->
<directory />
<!--产生的构件的文件名,默认值是${artifactId}-${version}。 -->
<finalName />
<!--当filtering开关打开时,使用到的过滤器属性文件列表 -->
<filters />
<!--子项目可以引用的默认插件信息。该插件配置项直到被引用时才会被解析或绑定到生命周期。给定插件的任何本地配置都会覆盖这里的配置 -->
<pluginManagement>
<!--使用的插件列表 。 -->
<plugins>
<!--plugin元素包含描述插件所需要的信息。 -->
<plugin>
<!--插件在仓库里的group ID -->
<groupId />
<!--插件在仓库里的artifact ID -->
<artifactId />
<!--被使用的插件的版本(或版本范围) -->
<version />
<!--是否从该插件下载Maven扩展(例如打包和类型处理器),由于性能原因,只有在真需要下载时,该元素才被设置成enabled。 -->
<extensions />
<!--在构建生命周期中执行一组目标的配置。每个目标可能有不同的配置。 -->
<executions>
<!--execution元素包含了插件执行需要的信息 -->
<execution>
<!--执行目标的标识符,用于标识构建过程中的目标,或者匹配继承过程中需要合并的执行目标 -->
<id />
<!--绑定了目标的构建生命周期阶段,如果省略,目标会被绑定到源数据里配置的默认阶段 -->
<phase />
<!--配置的执行目标 -->
<goals />
<!--配置是否被传播到子POM -->
<inherited />
<!--作为DOM对象的配置 -->
<configuration />
</execution>
</executions>
<!--项目引入插件所需要的额外依赖 -->
<dependencies>
<!--参见dependencies/dependency元素 -->
<dependency>
......
</dependency>
</dependencies>
<!--任何配置是否被传播到子项目 -->
<inherited />
<!--作为DOM对象的配置 -->
<configuration />
</plugin>
</plugins>
</pluginManagement>
<!--使用的插件列表 -->
<plugins>
<!--参见build/pluginManagement/plugins/plugin元素 -->
<plugin>
<groupId />
<artifactId />
<version />
<extensions />
<executions>
<execution>
<id />
<phase />
<goals />
<inherited />
<configuration />
</execution>
</executions>
<dependencies>
<!--参见dependencies/dependency元素 -->
<dependency>
......
</dependency>
</dependencies>
<goals />
<inherited />
<configuration />
</plugin>
</plugins>
</build>

<!--在列的项目构建profile,如果被激活,会修改构建处理 -->
<profiles>
<!--根据环境参数或命令行参数激活某个构建处理 -->
<profile>
<!--构建配置的唯一标识符。即用于命令行激活,也用于在继承时合并具有相同标识符的profile。 -->
<id />
<!--自动触发profile的条件逻辑。Activation是profile的开启钥匙。profile的力量来自于它 能够在某些特定的环境中自动使用某些特定的值;这些环境通过activation元素指定。activation元素并不是激活profile的唯一方式。 -->
<activation>
<!--profile默认是否激活的标志 -->
<activeByDefault />
<!--当匹配的jdk被检测到,profile被激活。例如,1.4激活JDK1.4,1.4.0_2,而!1.4激活所有版本不是以1.4开头的JDK。 -->
<jdk />
<!--当匹配的操作系统属性被检测到,profile被激活。os元素可以定义一些操作系统相关的属性。 -->
<os>
<!--激活profile的操作系统的名字 -->
<name>Windows XP</name>
<!--激活profile的操作系统所属家族(如 'windows') -->
<family>Windows</family>
<!--激活profile的操作系统体系结构 -->
<arch>x86</arch>
<!--激活profile的操作系统版本 -->
<version>5.1.2600</version>
</os>
<!--如果Maven检测到某一个属性(其值可以在POM中通过${名称}引用),其拥有对应的名称和值,Profile就会被激活。如果值 字段是空的,那么存在属性名称字段就会激活profile,否则按区分大小写方式匹配属性值字段 -->
<property>
<!--激活profile的属性的名称 -->
<name>mavenVersion</name>
<!--激活profile的属性的值 -->
<value>2.0.3</value>
</property>
<!--提供一个文件名,通过检测该文件的存在或不存在来激活profile。missing检查文件是否存在,如果不存在则激活 profile。另一方面,exists则会检查文件是否存在,如果存在则激活profile。 -->
<file>
<!--如果指定的文件存在,则激活profile。 -->
<exists>/usr/local/hudson/hudson-home/jobs/maven-guide-zh-to-production/workspace/
</exists>
<!--如果指定的文件不存在,则激活profile。 -->
<missing>/usr/local/hudson/hudson-home/jobs/maven-guide-zh-to-production/workspace/
</missing>
</file>
</activation>
<!--构建项目所需要的信息。参见build元素 -->
<build>
<defaultGoal />
<resources>
<resource>
<targetPath />
<filtering />
<directory />
<includes />
<excludes />
</resource>
</resources>
<testResources>
<testResource>
<targetPath />
<filtering />
<directory />
<includes />
<excludes />
</testResource>
</testResources>
<directory />
<finalName />
<filters />
<pluginManagement>
<plugins>
<!--参见build/pluginManagement/plugins/plugin元素 -->
<plugin>
<groupId />
<artifactId />
<version />
<extensions />
<executions>
<execution>
<id />
<phase />
<goals />
<inherited />
<configuration />
</execution>
</executions>
<dependencies>
<!--参见dependencies/dependency元素 -->
<dependency>
......
</dependency>
</dependencies>
<goals />
<inherited />
<configuration />
</plugin>
</plugins>
</pluginManagement>
<plugins>
<!--参见build/pluginManagement/plugins/plugin元素 -->
<plugin>
<groupId />
<artifactId />
<version />
<extensions />
<executions>
<execution>
<id />
<phase />
<goals />
<inherited />
<configuration />
</execution>
</executions>
<dependencies>
<!--参见dependencies/dependency元素 -->
<dependency>
......
</dependency>
</dependencies>
<goals />
<inherited />
<configuration />
</plugin>
</plugins>
</build>
<!--模块(有时称作子项目) 被构建成项目的一部分。列出的每个模块元素是指向该模块的目录的相对路径 -->
<modules />
<!--发现依赖和扩展的远程仓库列表。 -->
<repositories>
<!--参见repositories/repository元素 -->
<repository>
<releases>
<enabled />
<updatePolicy />
<checksumPolicy />
</releases>
<snapshots>
<enabled />
<updatePolicy />
<checksumPolicy />
</snapshots>
<id />
<name />
<url />
<layout />
</repository>
</repositories>
<!--发现插件的远程仓库列表,这些插件用于构建和报表 -->
<pluginRepositories>
<!--包含需要连接到远程插件仓库的信息.参见repositories/repository元素 -->
<pluginRepository>
<releases>
<enabled />
<updatePolicy />
<checksumPolicy />
</releases>
<snapshots>
<enabled />
<updatePolicy />
<checksumPolicy />
</snapshots>
<id />
<name />
<url />
<layout />
</pluginRepository>
</pluginRepositories>

<!--该元素描述了项目相关的所有依赖。 这些依赖组成了项目构建过程中的一个个环节。它们自动从项目定义的仓库中下载。要获取更多信息,请看项目依赖机制。 -->
<dependencies>
<!--参见dependencies/dependency元素 -->
<dependency>
......
</dependency>
</dependencies>

<!--不赞成使用. 现在Maven忽略该元素. -->
<reports />
<!--该元素包括使用报表插件产生报表的规范。当用户执行"mvn site",这些报表就会运行。 在页面导航栏能看到所有报表的链接。参见reporting元素 -->
<reporting>
......
</reporting>
<!--参见dependencyManagement元素 -->

<dependencyManagement>
<dependencies>
<!--参见dependencies/dependency元素 -->
<dependency>
......
</dependency>
</dependencies>
</dependencyManagement>

<!--参见distributionManagement元素 -->
<distributionManagement>
......
</distributionManagement>
<!--参见properties元素 -->
<properties />
</profile>
</profiles>
<!--模块(有时称作子项目) 被构建成项目的一部分。列出的每个模块元素是指向该模块的目录的相对路径 -->
<modules />
<!--发现依赖和扩展的远程仓库列表。 -->
<repositories>
<!--包含需要连接到远程仓库的信息 -->
<repository>
<!--如何处理远程仓库里发布版本的下载 -->
<releases>
<!--true或者false表示该仓库是否为下载某种类型构件(发布版,快照版)开启。 -->
<enabled />
<!--该元素指定更新发生的频率。Maven会比较本地POM和远程POM的时间戳。这里的选项是:always(一直),daily(默认,每日),interval:X(这里X是以分钟为单位的时间间隔),或者never(从不)。 -->
<updatePolicy />
<!--当Maven验证构件校验文件失败时该怎么做:ignore(忽略),fail(失败),或者warn(警告)。 -->
<checksumPolicy />
</releases>
<!-- 如何处理远程仓库里快照版本的下载。有了releases和snapshots这两组配置,POM就可以在每个单独的仓库中,为每种类型的构件采取不同的
策略。例如,可能有人会决定只为开发目的开启对快照版本下载的支持。参见repositories/repository/releases元素 -->
<snapshots>
<enabled />
<updatePolicy />
<checksumPolicy />
</snapshots>
<!--远程仓库唯一标识符。可以用来匹配在settings.xml文件里配置的远程仓库 -->
<id>banseon-repository-proxy</id>
<!--远程仓库名称 -->
<name>banseon-repository-proxy</name>
<!--远程仓库URL,按protocol://hostname/path形式 -->
<url>http://192.168.1.169:9999/repository/</url>
<!-- 用于定位和排序构件的仓库布局类型-可以是default(默认)或者legacy(遗留)。Maven 2为其仓库提供了一个默认的布局;然
而,Maven 1.x有一种不同的布局。我们可以使用该元素指定布局是default(默认)还是legacy(遗留)。 -->
<layout>default</layout>
</repository>
</repositories>
<!--发现插件的远程仓库列表,这些插件用于构建和报表 -->
<pluginRepositories>
<!--包含需要连接到远程插件仓库的信息.参见repositories/repository元素 -->
<pluginRepository>
......
</pluginRepository>
</pluginRepositories>


<!--该元素描述了项目相关的所有依赖。 这些依赖组成了项目构建过程中的一个个环节。它们自动从项目定义的仓库中下载。要获取更多信息,请看项目依赖机制。 -->
<dependencies>
<dependency>
<!--依赖的group ID -->
<groupId>org.apache.maven</groupId>
<!--依赖的artifact ID -->
<artifactId>maven-artifact</artifactId>
<!--依赖的版本号。 在Maven 2里, 也可以配置成版本号的范围。 -->
<version>3.8.1</version>
<!-- 依赖类型,默认类型是jar。它通常表示依赖的文件的扩展名,但也有例外。一个类型可以被映射成另外一个扩展名或分类器。类型经常和使用的打包方式对应,
尽管这也有例外。一些类型的例子:jar,war,ejb-client和test-jar。如果设置extensions为 true,就可以在 plugin里定义新的类型。所以前面的类型的例子不完整。 -->
<type>jar</type>
<!-- 依赖的分类器。分类器可以区分属于同一个POM,但不同构建方式的构件。分类器名被附加到文件名的版本号后面。例如,如果你想要构建两个单独的构件成
JAR,一个使用Java 1.4编译器,另一个使用Java 6编译器,你就可以使用分类器来生成两个单独的JAR构件。 -->
<classifier></classifier>
<!--依赖范围。在项目发布过程中,帮助决定哪些构件被包括进来。欲知详情请参考依赖机制。 - compile :默认范围,用于编译 - provided:类似于编译,但支持你期待jdk或者容器提供,类似于classpath
- runtime: 在执行时需要使用 - test: 用于test任务时使用 - system: 需要外在提供相应的元素。通过systemPath来取得
- systemPath: 仅用于范围为system。提供相应的路径 - optional: 当项目自身被依赖时,标注依赖是否传递。用于连续依赖时使用 -->
<scope>test</scope>
<!--仅供system范围使用。注意,不鼓励使用这个元素,并且在新的版本中该元素可能被覆盖掉。该元素为依赖规定了文件系统上的路径。需要绝对路径而不是相对路径。推荐使用属性匹配绝对路径,例如${java.home}。 -->
<systemPath></systemPath>
<!--当计算传递依赖时, 从依赖构件列表里,列出被排除的依赖构件集。即告诉maven你只依赖指定的项目,不依赖项目的依赖。此元素主要用于解决版本冲突问题 -->
<exclusions>
<exclusion>
<artifactId>spring-core</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
<!--可选依赖,如果你在项目B中把C依赖声明为可选,你就需要在依赖于B的项目(例如项目A)中显式的引用对C的依赖。可选依赖阻断依赖的传递性。 -->
<optional>true</optional>
</dependency>
</dependencies>
<!--不赞成使用. 现在Maven忽略该元素. -->
<reports></reports>
<!--该元素描述使用报表插件产生报表的规范。当用户执行"mvn site",这些报表就会运行。 在页面导航栏能看到所有报表的链接。 -->
<reporting>
<!--true,则,网站不包括默认的报表。这包括"项目信息"菜单中的报表。 -->
<excludeDefaults />
<!--所有产生的报表存放到哪里。默认值是${project.build.directory}/site。 -->
<outputDirectory />
<!--使用的报表插件和他们的配置。 -->
<plugins>
<!--plugin元素包含描述报表插件需要的信息 -->
<plugin>
<!--报表插件在仓库里的group ID -->
<groupId />
<!--报表插件在仓库里的artifact ID -->
<artifactId />
<!--被使用的报表插件的版本(或版本范围) -->
<version />
<!--任何配置是否被传播到子项目 -->
<inherited />
<!--报表插件的配置 -->
<configuration />
<!--一组报表的多重规范,每个规范可能有不同的配置。一个规范(报表集)对应一个执行目标 。例如,有1,2,3,4,5,6,7,8,9个报表。1,2,5构成A报表集,对应一个执行目标。2,5,8构成B报表集,对应另一个执行目标 -->
<reportSets>
<!--表示报表的一个集合,以及产生该集合的配置 -->
<reportSet>
<!--报表集合的唯一标识符,POM继承时用到 -->
<id />
<!--产生报表集合时,被使用的报表的配置 -->
<configuration />
<!--配置是否被继承到子POMs -->
<inherited />
<!--这个集合里使用到哪些报表 -->
<reports />
</reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>
<!-- 继承自该项目的所有子项目的默认依赖信息。这部分的依赖信息不会被立即解析,而是当子项目声明一个依赖(必须描述group ID和 artifact
ID信息),如果group ID和artifact ID以外的一些信息没有描述,则通过group ID和artifact ID 匹配到这里的依赖,并使用这里的依赖信息。 -->
<dependencyManagement>
<dependencies>
<!--参见dependencies/dependency元素 -->
<dependency>
......
</dependency>
</dependencies>
</dependencyManagement>
<!--项目分发信息,在执行mvn deploy后表示要发布的位置。有了这些信息就可以把网站部署到远程服务器或者把构件部署到远程仓库。 -->
<distributionManagement>
<!--部署项目产生的构件到远程仓库需要的信息 -->
<repository>
<!--是分配给快照一个唯一的版本号(由时间戳和构建流水号)?还是每次都使用相同的版本号?参见repositories/repository元素 -->
<uniqueVersion />
<id>banseon-maven2</id>
<name>banseon maven2</name>
<url>file://${basedir}/target/deploy</url>
<layout />
</repository>
<!--构件的快照部署到哪里?如果没有配置该元素,默认部署到repository元素配置的仓库,参见distributionManagement/repository元素 -->
<snapshotRepository>
<uniqueVersion />
<id>banseon-maven2</id>
<name>Banseon-maven2 Snapshot Repository</name>
<url>scp://svn.baidu.com/banseon:/usr/local/maven-snapshot</url>
<layout />
</snapshotRepository>
<!--部署项目的网站需要的信息 -->
<site>
<!--部署位置的唯一标识符,用来匹配站点和settings.xml文件里的配置 -->
<id>banseon-site</id>
<!--部署位置的名称 -->
<name>business api website</name>
<!--部署位置的URL,按protocol://hostname/path形式 -->
<url>
scp://svn.baidu.com/banseon:/var/www/localhost/banseon-web
</url>
</site>
<!--项目下载页面的URL。如果没有该元素,用户应该参考主页。使用该元素的原因是:帮助定位那些不在仓库里的构件(由于license限制)。 -->
<downloadUrl />
<!--如果构件有了新的group ID和artifact ID(构件移到了新的位置),这里列出构件的重定位信息。 -->
<relocation>
<!--构件新的group ID -->
<groupId />
<!--构件新的artifact ID -->
<artifactId />
<!--构件新的版本号 -->
<version />
<!--显示给用户的,关于移动的额外信息,例如原因。 -->
<message />
</relocation>
<!-- 给出该构件在远程仓库的状态。不得在本地项目中设置该元素,因为这是工具自动更新的。有效的值有:none(默认),converted(仓库管理员从
Maven 1 POM转换过来),partner(直接从伙伴Maven 2仓库同步过来),deployed(从Maven 2实例部 署),verified(被核实时正确的和最终的)。 -->
<status />
</distributionManagement>
<!--以值替代名称,Properties可以在整个POM中使用,也可以作为触发条件(见settings.xml配置文件里activation元素的说明)。格式是<name>value</name>。 -->
<properties />
</project>

基础配置

一、根元素和必要配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<project xmlns = "http://maven.apache.org/POM/4.0.0"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">

<!-- 模型版本 -->
<modelVersion>4.0.0</modelVersion>
<!-- 公司或者组织的唯一标志,也是打包成jar包路径的依据 -->
<!-- 例如com.companyname.project-group,maven打包jar包的路径:/com/companyname/project-group -->
<groupId>com.companyname.project-group</groupId>

<!-- 项目的唯一ID,一个groupId下面可能多个项目,就是靠artifactId来区分的 -->
<artifactId>project</artifactId>

<!-- 项目当前版本,格式为:主版本.次版本.增量版本-限定版本号 -->
<version>1.0</version>

<!--项目产生的构件类型,包括jar、war、ear、pom等 -->
<packaging>jar</packaging>
</project>

project是pom文件的根元素,project下有modelVersion、groupId、artifactId、version、packaging等重要的元素。其中,groupId、artifactId、version三个元素用来定义一个项目的坐标,也就是说,一个maven仓库中,完全相同的一组groupId、artifactId、version,只能有一个项目。

  • project:整个pom配置文件的根元素,所有的配置都是写在project元素里面的;
  • modelVersion:指定了当前POM模型的版本,对于Maven2及Maven 3来说,它只能是4.0.0;
  • groupId:这是项目组的标识。它在一个组织或者项目中通常是唯一的。
  • artifactId:这是项目的标识,通常是工程的名称。它在一个项目组(group)下是唯一的。
  • version:这是项目的版本号,用来区分同一个artifact的不同版本。
  • packaging:这是项目产生的构件类型,即项目通过maven打包的输出文件的后缀名,包括jar、war、ear、pom等。

二、父项目和parent元素

maven 支持继承功能。子 POM 可以使用 parent 指定父 POM ,然后继承其配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--父项目的坐标,坐标包括group ID,artifact ID和version。 -->
<!--如果项目中没有规定某个元素的值,那么父项目中的对应值即为项目的默认值 -->
<parent>
<!--被继承的父项目的构件标识符 -->
<artifactId>com.companyname.project-group</artifactId>
<!--被继承的父项目的全球唯一标识符 -->
<groupId>base-project</groupId>
<!--被继承的父项目的版本 -->
<version>1.0.1-RELEASE</version>
<!-- 父项目的pom.xml文件的相对路径,默认值是../pom.xml。 -->
<!-- 寻找父项目的pom:构建当前项目的地方--)relativePath指定的位置--)本地仓库--)远程仓库 -->
<relativePath>../my-parent</relativePath>
</parent>

<artifactId>my-project</artifactId>
</project>
  • relativePath - 注意 relativePath 元素。在搜索本地和远程存储库之前,它不是必需的,但可以用作 maven 的指示符,以首先搜索给定该项目父级的路径。

所有的pom都继承自一个父pom(Super POM)。父pom包含了一些可以被继承的默认设置,如果项目的pom中没有设置这些元素,就会使用父pom中设置。例如,Super POM中配置了默认仓库repo1.maven.org/maven2,这样哪怕… pom文件约定大于配置的原则,就是通过在Super POM中预定义了一些配置信息来实现的。

Maven使用effective pom(Super pom加上工程自己的配置)来执行相关的目标,它帮助开发者在pom.xml中做尽可能少的配置。当然,这些配置也可以被重写。

parent元素可以指定父pom。用户可以通过增加parent元素来自定义一个父pom,从而继承该pom的配置。parent元素中包含一些子元素,用来定位父项目和父项目的pom文件位置。

  • parent:用于指定父项目;
  • groupId:parent的子元素,父项目的groupId,用于定位父项目;
  • artifactId:parent的子元素,父项目的artifactId,用于定位父项目;
  • version:parent的子元素,父项目的version,用于定位父项目;
  • relativePath:parent的子元素,用于定位父项目pom文件的位置。

三、modules——子模块列表

此标签在父工程的pom.xml中表示子模块的位置,标签内元素为。以当前父工程所在的文件夹为基准,中填写子模块的相对路径
如果是父工程的子工程,则直接填写其文件夹名即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.codehaus.mojo</groupId>
<artifactId>my-parent</artifactId>
<version>2.0</version>
<packaging>pom</packaging>

<modules>
<module>my-project</module>
<module>another-project</module>
<module>third-project/pom-example.xml</module>
</modules>
</project>

四、dependencyManagement——子项目的默认依赖信息

pom文件中:

  • 通过dependencyManagement来声明依赖
  • 通过dependencies元素来管理依赖。

dependencyManagement下的子元素:

  • 一个直接的子元素dependencies,其配置和dependencies子元素是完全一致的;

dependencies下的子元素:

  • 一类直接的子元素:dependency。一个dependency子元素表示一个依赖项目。

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

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

dependencyManagement 主要是为了统一管理依赖包的版本,确保所有子项目使用的版本一致,类似的还有pluginspluginManagement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<!-- 继承自该项目的所有子项目的默认依赖信息,这部分的依赖信息不会被立即解析。 -->
<!-- 当子项目声明一个依赖,如果group ID和artifact ID以外的一些信息没有描述,则使用这里的依赖信息 -->
<dependencyManagement>
<dependencies>
<!--参见dependencies/dependency元素 -->
<dependency>
......
</dependency>
</dependencies>
</dependencyManagement>
...
</project>

五、项目依赖相关信息

1、依赖的配置

① maven解析依赖信息时会到本地仓库中取查找被依赖的jar包

  • 对于本地仓库中没有的会去中央仓库去查找maven坐标来获取jar包,获取到jar之后会下载到本地仓库
  • 对于中央仓库也找不到依赖的jar包的时候,就会编译失败了

② 如果依赖的是自己或者团队开发的maven工程,需要先使用install命令把被依赖的maven工程的jar包导入到本地仓库中

举例:现在我再创建第二个maven工程HelloFriend,其中用到了第一个Hello工程里类的sayHello(String name)方法。我们在给HelloFriend项目使用 mvn compile命令进行编译的时候,会提示缺少依赖Hello的jar包。怎么办呢?

到第一个maven工程中执行 mvn install后,你再去看一下本地仓库,你会发现有了Hello项目的jar包。一旦本地仓库有了依赖的maven工程的jar包后,你再到HelloFriend项目中使用 mvn compile命令的时候,可以成功编译

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
<project>
...
<!--该元素描述了项目相关的所有依赖。 这些依赖自动从项目定义的仓库中下载 -->
<dependencies>
<dependency>
<!------------------- 依赖坐标 ------------------->
<!--依赖项目的坐标三元素:groupId + artifactId + version -->
<groupId>org.apache.maven</groupId>
<artifactId>maven-artifact</artifactId>
<version>3.8.1</version>

<!------------------- 依赖类型 ------------------->
<!-- 依赖类型,默认是jar。通常表示依赖文件的扩展名,但有例外。一个类型可以被映射成另外一个扩展名或分类器 -->
<!-- 类型经常和使用的打包方式对应,尽管这也有例外,一些类型的例子:jar,war,ejb-client和test-jar -->
<!-- 如果设置extensions为true,就可以在plugin里定义新的类型 -->
<type>jar</type>
<!-- 依赖的分类器。分类器可以区分属于同一个POM,但不同构建方式的构件。分类器名被附加到文件名的版本号后面 -->
<!-- 如果想将项目构建成两个单独的JAR,分别使用Java 4和6编译器,就可以使用分类器来生成两个单独的JAR构件 -->
<classifier></classifier>

<!------------------- 依赖传递 ------------------->
<!--依赖排除,即告诉maven只依赖指定的项目,不依赖该项目的这些依赖。此元素主要用于解决版本冲突问题 -->
<exclusions>
<exclusion>
<artifactId>spring-core</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
<!-- 可选依赖,用于阻断依赖的传递性。如果在项目B中把C依赖声明为可选,那么依赖B的项目中无法使用C依赖 -->
<optional>true</optional>

<!------------------- 依赖范围 ------------------->
<!--依赖范围。在项目发布过程中,帮助决定哪些构件被包括进来
- compile:默认范围,用于编译; - provided:类似于编译,但支持jdk或者容器提供,类似于classpath
- runtime: 在执行时需要使用; - systemPath: 仅用于范围为system。提供相应的路径
- test: 用于test任务时使用; - system: 需要外在提供相应的元素。通过systemPath来取得
- optional: 当项目自身被依赖时,标注依赖是否传递。用于连续依赖时使用 -->
<scope>test</scope>
<!-- 该元素为依赖规定了文件系统上的路径。仅供scope设置system时使用。但是不推荐使用这个元素 -->
<!-- 不推荐使用绝对路径,如果必须要用,推荐使用属性匹配绝对路径,例如${java.home} -->
<systemPath></systemPath>
</dependency>
</dependencies>
...
</project>

根元素project下的dependencies可以包含一个或者多个dependency元素,以声明一个或者多个项目依赖。每个依赖可以包含的元素有:

  • grounpId、artifactId和version:以来的基本坐标,对于任何一个依赖来说,基本坐标是最重要的,Maven根据坐标才能找到需要的依赖。
  • type:依赖的类型,对于项目坐标定义的packaging。大部分情况下,该元素不必声明,其默认值为jar
  • scope:依赖的范围
  • optional:标记依赖是否可选
  • exclusions:用来排除传递性依赖

2、依赖范围

依赖范围就是用来控制依赖和三种classpath(编译classpath,测试classpath、运行classpath)的关系,Maven有如下几种依赖范围:

  • **compile:**编译依赖范围。默认值,适用于所有阶段(开发、测试、部署、运行),本jar会一直存在所有阶段。典型的例子是spring-code,在编译、测试和运行的时候都需要使用该依赖。
  • test: 测试依赖范围只在测试时使用,用于编译和运行测试代码。不会随项目发布。典型的例子是Jnuit,它只有在编译测试代码及运行测试的时候才需要。
  • **provided:**已提供依赖范围。只在开发、测试阶段使用。典型的例子是servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器以及提供,就不需要Maven重复地引入一遍。
  • **runtime:**运行时依赖范围。只在运行、测试时使用。典型的例子是JDBC驱动实现,项目主代码的编译只需要JDK提供的JDBC接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC驱动。
  • system:系统依赖范围。该依赖与三种classpath的关系,和provided依赖范围完全一致,但是,使用system范围的依赖时必须通过systemPath元素显示地指定依赖文件的路径。由于此类依赖不是通过Maven仓库解析的,而且往往与本机系统绑定,可能构成构建的不可移植,因此应该谨慎使用。systemPath元素可以引用环境变量,如:
1
2
3
4
5
6
7
<dependency>
<groupId>javax.sql</groupId>
<artifactId>jdbc-stdext</artifactId>
<Version>2.0</Version>
<scope>system</scope>
<systemPath>${java.home}/lib/rt.jar</systemPath>
</dependency>
  • **import:**导入依赖范围。该依赖范围不会对三种classpath产生实际的影响。

上述除import以外的各种依赖范围与三种classpath的关系如下:

3、依赖的传递性

WebMavenDemo项目依赖JavaMavenService1 JavaMavenService1项目依赖JavaMavenService2

pom.xml文件配置好依赖关系后,必须首先mvn install后,依赖的jar包才能使用。

  • WebMavenDemo的pom.xml文件想能编译通过,JavaMavenService1必须mvn install
  • JavaMavenService的pom.xml文件想能编译通过,JavaMavenService2必须mvn install

传递性:

注意:非compile范围的依赖是不能传递的。

依赖传递范围:

假设A依赖于B,B依赖于C,我们说A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。第一直接依赖和第二直接依赖的范围决定了传递性依赖的范围,如下图所示,最左边一行表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递依赖范围。

image.png

从上图中,我们可以发现这样的规律:

  • 当第二直接依赖的范围是compile的时候,传递性依赖的范围与第一直接依赖的范围一致;
  • 当第二直接依赖的范围是test的时候,依赖不会得以传递;
  • 当第二直接依赖的范围是provided的时候,只传递第一直接依赖范围也为provided的依赖,切传递依赖的范围同样为provided;
  • 当第二直接依赖的范围是runtime的时候,传递性依赖的范围与第一直接依赖的范围一致,但compile列外,此时传递性依赖范围为runtime.

依赖版本的原则:

1、路径最短者优先原则

Service2的log4j的版本是1.2.7版本,Service1排除了此包的依赖,自己加了一个Log4j的1.2.9的版本,那么WebMavenDemo项目遵守路径最短优先原则,Log4j的版本和Sercive1的版本一致。

2、路径相同先声明优先原则

这种场景依赖关系发生了变化,WebMavenDemo项目依赖Sercive1和Service2,它俩是同一个路径,那么谁在WebMavenDemo的pom.xml中先声明的依赖就用谁的版本。

可选依赖

如图,项目中A依赖B,B依赖于X和Y,如果所有这三个的范围都是compile的话,那么X和Y就是A的compile范围的传递性依赖,但是如果我想X,Y不作为A的传递性依赖,不给他用的话。就需要下面提到的配置可选依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<project>  
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-b</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.10</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</groupId>
<version>8.4-701.jdbc3</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>

配置也简单,在依赖里面添加

1
<optional>true</optional>

就表示可选依赖了,这样A如果想用X,Y就要直接显示的添加依赖了。

排除依赖

有时候你引入的依赖中包含你不想要的依赖包,你想引入自己想要的,这时候就要用到排除依赖了,比如下图中spring-boot-starter-web自带了logback这个日志包,我想引入log4j2的,所以我先排除掉logback的依赖包,再引入想要的包就行了

排除依赖代码结构:

1
2
3
4
5
6
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>

这里注意:声明exclustion的时候只需要groupId和artifactId,而不需要version元素,这是因为只需要groupId和artifactId就能唯一定位依赖图中的某个依赖。

4、归类依赖

有时候我们引入的很多依赖包,他们都来自同一个项目的不同模块,所以他们的版本号都一样,这时候我们可以用属性来统一管理版本号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<project>  
<modelVersion>4.0.0</modelVersion>
<groupId>com.juven.mvnbook.account</groupId>
<artifactId>accout-email</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<springframework.version>1.5.6</springframework.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${springframework.version}</version>
</dependency>
</dependencies>
</project>

如图所示,先通过

1
2
3
</properties>
这里定义你先要的版本
</properties>

来定义,然后在下面依赖使用${}来引入你的属性。

六、properties——属性列表

properties里面可以定义用户自己的属性值,这些属性值可以在POM文件的任何地方同通过${x}的方式来引用,例如可以通过如下方式管理jar包版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<project>
...
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<mysql.version>8.0.21</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!--引用属性-->
<version>${mysql.version}</version>
</dependency>
</dependencies>
...
</project>

${X}方式除了可以应用自定义的propertes属性值,还可以引用一下属性:

路径 说明 示例
${env.X} 平台系统环境变量 windows、或者linux系统的环境变量,例如:${env.JAVA_HOME}可以获取JAVA_HOME的路径。命令行运行:mvn help:system查看所有可用平台系统环境变量
${project.X} pom文件中project根标签下的变量 按照官方文档的解释:当前POM文件中project根标签下的标签值都可以通过${project.X}的方式来获取。例如:${project.version}。具体哪些标签可参考官方文档:maven-model
${settings.X} maven安装目录:conf->settings配置文件里面的属性 例如:通过${settings.localRepository}可以获取settings配置文件中的本地仓库路径
${X} Java系统属性 例如:通过${java.runtime.version}可以获取java运行版本。命令行运行:mvn help:system查看所有可用Java系统属性

数据完整性约束

  • 表的数据有一定的取值范围和联系,多表之间的数据有时也有一定的参照关系。
  • 在创建表和修改表时,可通过定义约束条件来保证数据的完整性和一致性。
  • 约束条件是一些规则,在对数据进行插入、删除和修改时要对这些规则进行验证,从而起到约束作用。

完整性约束分类

  • 域完整性约束(非空not null,检查check)

  • 实体完整性约束(唯一unique,主键primary key)

  • 参照完整性约束(外键foreign key)

约束类型 说明
主键约束(Primary Key) 要求主键列数据唯一,并且不允许为空。主键可以包含表的一列或多列,如果包含表的多列,则需要在表级定义。
唯一约束(Unique) 要求该列唯一,允许为空,但只能出现一个空值
检查约束(Check) 某列取值范围限制、格式限制等,如年龄的约束
非空约束(not null) 某类内容不能为空
外键约束(Foreign Key) 用于两表间建立关系,需要指定引用主表的那列。外键通常用来约束两个表之间的数据关系,定义外键的那张表称为子表,另一张表称为主表。 在表的创建过程中,应该先创建主表,后创建子表。

三种完整性约束的区别

  • 域完整性约束:字段约束
  • 实体完整性约束:行和行之间的约束
  • 引用完整性约束:表和表之间的约束

创建约束的时机

  • 在建表的同时创建
  • 建表后创建
  • 约束从作用上分类,可以分成两大类:
    • 表级约束:可以约束表中的任意一列或多列。可以定义出了Not Null以外的任何约束。
    • 列级约束:只能约束其所在的某一列。可以定义任何约束。

命名规则推荐采用:约束类型_约束字段

非空约束 NN_表名列名
唯一约束 UK_表名_列名
主键约束 PK_表名
外键约束 FK_表名_列名
检查约束 CK_表名_列名

数据库表的约束

主键约束

  • 主键约束是数据库中最重要的一种约束。在关系中,主键值不可为空,也不允许出现重复,即关系要满足实体完整性规则。
  • 主键从功能上看相当于非空且唯一
  • 一个表中只允许一个主键
  • 主键是表中能够唯一确定一个行数据的字段
  • 主键字段可以是单字段或者是多字段的组合
1
2
3
4
5
Oracle为主键创建对应的唯一性索引
create table t3(
id number(4), --primary key,
constraint t3_pk primary key(id)
)

唯一性约束

  • 唯一性约束条件确保所在的字段或者字段组合不出现重复值

  • 唯一性约束条件的字段允许出现空值,且可以多个空值

1
2
3
4
5
6
7
8
9
Oracle将为唯一性约束条件创建对应的唯一性索引
CREATE TABLE employees(
id NUMBER(6),
name VARCHAR2(25) NOT NULL UNIQUE,
email VARCHAR2(25),
salary NUMBER(8,2),
hire_date DATE NOT NULL,
CONSTRAINT emp_email_uk UNIQUE(email)
);

非空约束

  • 确保字段值不允许为空
  • 只能在字段级定义
1
2
3
4
5
6
CREATE TABLE employees(
employee_id NUMBER(6),
name VARCHAR2(25) NOT NULL,
salary NUMBER(8,2),
hire_date DATE CONSTRAINT emp_hire_date_nn NOT NULL
)

check约束

  • Check约束用于对一个属性的值加以限制
  • 在check中定义检查的条件表达式,数据需要符合设置的条件
1
2
3
4
5
6
7
create table emp3
( id number(4) primary key,
age number(2) check(age > 0 and age < 100),
salary number(7,2),
sex char(1),
constraint salary_check check(salary > 0)
)
  • 在这种约束下,插入记录或修改记录时,系统要测试新的记录的值是否满足条件

外键约束

  • 外键是表中的一个列,其值必须在另一表的主键或者唯一键中列出
  • 作为主键的表称为“主表”,作为外键的关系称为“依赖表”
  • 外键参照的是主表的主键或者唯一键
  • 对于主表的删除和修改主键值的操作,会对依赖关系产生影响,以删除为例:当要删除主表的某个记录(即删除一个主键值,那么对依赖的影响可采取下列3种做法:
    • RESTRICT方式:只有当依赖表中没有一个外键值与要删除的主表中主键值相对应时,才可执行删除操作。
    • CASCADE方式:将依赖表中所有外键值与主表中要删除的主键值相对应的记录一起删除
    • SET NULL方式:将依赖表中所有与主表中被删除的主键值相对应的外键值设为空值
    • FOREIGN KEY (DEPTNO) REFERENCES DEPT(DEPTNO)
    • [ON DELETE [CASCADE|SET NULL]] 如省略on短语,缺省为第一中处理方式。

添加约束和删除约束

  • 添加约束
1
2
ALTER TABLE 表名 
ADD CONSTRAINT 约束名 约束类型 具体的约束说明
  • 删除约束
1
2
ALTER TABLE 表名 
DROP CONSTRAINT 约束名
  • 可增加或删除约束,但不能直接修改

索引index

  • 索引类型默认采用B树数据结构,数据全部集中在叶子节点
  • 索引的创建有两种情况
    • 自动: 当在表上定义一个PRIMARY KEY 或者UNIQUE 约束条件时,Oracle数据库自动创建一个对应的唯一索引.
    • 手动: 用户可以创建索引以加速查询

相关语法

  • 在一列或者多列上创建索引.
1
CREATE INDEX index  ON table (column[, column]...); 

下面的索引将会提高对EMP表基于 ENAME 字段的查询速度.

1
2
CREATE INDEX  emp_last_name_idx
ON emp (ename)
  • 通过DROP INDEX 命令删掉一个索引.
1
DROP INDEX index;
  • 删掉 UPPER_LAST_NAME_IDX 索引.
1
DROP INDEX upper_last_name_idx;

序列sequence

序列是oracle专有的对象,它用来产生一个自动递增的数列

创建序列的语法:

1
2
3
4
5
6
7
create sequence seq-name
increment by n
start with n
maxvalue n|nomaxvalue 10^27 or -1
minvalue n|no minvalue
cycle|nocycle
cache n|nocache

实例:

1
create sequence seq_empcopy_id start with 1 increment by 1;

使用序列

1
2
3
select seq_empcopy_id.nextval from dual
insert into empcopy (empno,ename)
values (seq_empcopy_id.nextval, ‘TEST’);

查看序列状态

1
select seq_empcopy_id.currval from dual

删除序列

1
drop sequence seq_empcopy_id;

视图view

  1. 定义:
  • 视图是从若干基本表和(或)其他视图构造出来的表。
  • 在创建一个视图时,只是存放的视图的定义,也即是动态检索数据的查询语句,而并不存放视图对应的数据
  • 在用户使用视图时才去求相对应的数据。所以视图被称作“虚表”
  1. 作用:
  • 可以限制对数据的访问,可以给用户授予表的特定部分的访问权限而不是整个表的访问权限。
  • 可以使复杂的查询变的简单。在编写查询后,可以方便地重用它而不必知道他的基本查询细节。
  • 提供了对相同数据的不同显示

创建视图

1
2
3
4
CREATE [OR REPLACE] VIEW view
[(alias[, alias]...)]
AS subquery
[WITH READ ONLY];

查询视图

1
select * from viewname

不需要再写完全的Select查询语句,

删除视图

1
Drop view viewname;

删掉视图不会导致数据丢失,因为视图是基于数据库表的一个查询

总结

  • 视图是一个虚拟表,对应一条SELECT语句,可将它的输出看作一个表
  • 视图不存储数据
  • 改变基本表的数据,也会反应到基于该表的视图上
  • 视图可以基于基本表的若干行,若干列
  • 视图可以基于一个表、多个表,甚至是基于其他的视图
  • 使用视图可以提高数据访问的安全性,只显示指定的行列数据
  • 使用视图可以降低查询的难度,定制数据显示
  • 可以对视图进行CRUD操作,实际上是对基本表的CRUD操作
  • 如果视图对应多个表,一般不允许添加操作,可以通过触发器解决
  • 使用with read only定义只读视图

事务

一个操作序列。这些操作要么都做,要么都不做,是一个不可分割的工作单位,是数据库环境中的逻辑工作单位。

事务是为了保证数据库的完整性

在oracle中,没有事务开始的语句。一个Transaction起始于一条DML(Insert、Update和Delete )语句,结束于以下的几种情况:

  • 用户显式执行Commit语句提交操作或Rollback语句回退。

  • 当执行DDL(Create、Alter、Drop)语句事务自动提交。

  • 用户正常断开连接时,Transaction自动提交。

  • 系统崩溃或断电时事务自动回退。

  • Commit表示事务成功地结束,此时告诉系统,数据库要进入一个新的正确状态,该事务对数据库的所有更新都以交付实施。每个Commit语句都可以看成是一个事务成功的结束,同时也是另一个事务的开始。

  • Rollback表示事务不成功的结束,此时告诉系统,已发生错误,数据库可能处在不正确的状态,该事务对数据库的更新必须被撤销,数据库应恢复该事务到初始状态。每个Rollback语句同时也是另一个事务的开始。

  • 一旦执行了commit语句,将目前对数据库的操作提交给数据库(实际写入DB),以后就不能用rollback进行撤销。

  • 执行一个 DDL ,DCL语句或从 SQL*Plus正常退出,都会自动执行commit命令。

  • 提交或回滚前数据的状态

    • 以前的数据可恢复
    • 当前的用户可以看到DML操作的结果
    • 其他用户不能看到DML操作的结果
    • 被操作的数据被锁住,其他用户不能修改这些数据
  • 提交后数据的状态

  • 数据的修改被永久写在数据库中.

    • 数据以前的状态永久性丢失.
  • 所有的用户都能看到操作后的结果.

    • 记录锁被释放,其他用户可操作这些记录.
  • 回滚后数据的状态

    • 语句将放弃所有的数据修改
    • 修改的数据被回退.
    • 恢复数据以前的状态.
    • 行级锁被释放.

正则表达式

基本语法

single char quantifiers(数量) position(位置)
\d 匹配数字 * 0个或者更多 ^一行的开头
\w 匹配word(数字、字母) + 1个或更多,至少1个 $一行的结尾
\W 匹配word(数字、字母) ? 0个或1个,一个Optional \b 单词”结界”(word bounds)
\s 匹配white space(包括空格、tab等) {min,max}出现次数在一个范围内
\S 匹配white space(包括空格、tab等) {n}匹配出现n次的
. 匹配任何,任何的字符

常见的正则表达式

1、检验密码强度

密码的强度必须是包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间。

1
^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$

2. 校验中文

字符串仅能是中文。

1
^[\\u4e00-\\u9fa5]{0,}$复制代码

3. 由数字、26个英文字母或下划线组成的字符串

1
^\\w+$复制代码

4. 校验E-Mail 地址

同密码一样,下面是E-mail地址合规性的正则检查语句。

1
[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[\\w](?:[\\w-]*[\\w])?复制代码

5. 校验身份证号码

下面是身份证号码的正则校验。15 或 18位。

15位:

1
^[1-9]\\d{7}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}$复制代码

18位:

1
^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([0-9]|X)$复制代码

6. 校验日期

“yyyy-mm-dd“ 格式的日期校验,已考虑平闰年。

1
^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$复制代码

7. 校验金额

金额校验,精确到2位小数。

1
^[0-9]+(.[0-9]{2})?$复制代码

8. 校验手机号

下面是国内 13、15、18开头的手机号正则表达式。

1
^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\\d{8}$复制代码

9. 判断IE的版本

IE目前还没被完全取代,很多页面还是需要做版本兼容,下面是IE版本检查的表达式。

1
^.*MSIE [5-8](?:\\.[0-9]+)?(?!.*Trident\\/[5-9]\\.0).*$复制代码

10. 校验IP-v4地址

IP4 正则语句。

1
\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b复制代码

11. 校验IP-v6地址

IP6 正则语句。

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]))复制代码

12. 检查URL的前缀

应用开发中很多时候需要区分请求是HTTPS还是HTTP,通过下面的表达式可以取出一个url的前缀然后再逻辑判断。

1
2
3
4
if (!s.match(/^[a-zA-Z]+:\\/\\//))
{
s = 'http://' + s;
}复制代码

13. 提取URL链接

下面的这个表达式可以筛选出一段文本中的URL。

1
^(f|ht){1}(tp|tps):\\/\\/([\\w-]+\\.)+[\\w-]+(\\/[\\w- ./?%&=]*)?复制代码

14. 文件路径及扩展名校验

验证文件路径和扩展名

1
^([a-zA-Z]\\:|\\\\)\\\\([^\\\\]+\\\\)*[^\\/:*?"<>|]+\\.txt(l)?$复制代码

15. 提取Color Hex Codes

有时需要抽取网页中的颜色代码,可以使用下面的表达式。

1
\\#([a-fA-F]|[0-9]){3,6}复制代码

16. 提取网页图片

假若你想提取网页中所有图片信息,可以利用下面的表达式。

1
\\< *[img][^\\>]*[src] *= *[\\"\\']{0,1}([^\\"\\'\\ >]*)复制代码

17. 提取页面超链接

提取html中的超链接。

1
(]*)(href="https?://)((?!(?:(?:www\\.)?'.implode('|(?:www\\.)?', $follow_list).'))[^"]+)"((?!.*\\brel=)[^>]*)(?:[^>]*)>复制代码

18. 精炼CSS

通过下面的表达式,可以搜索相同属性值的CSS,从而达到精炼代码的目的。

1
^\\s*[a-zA-Z\\-]+\\s*[:]{1}\\s[a-zA-Z0-9\\s.#]+[;]{1}复制代码

19. 抽取注释

如果你需要移除HMTL中的注释,可以使用如下的表达式。

1
复制代码

20. 匹配HTML标签

通过下面的表达式可以匹配出HTML中的标签。

1
\\s]+))?)+\\s*|\\s*)/?>复制代码

参考文章

正则表达式不要背 - 掘金 (juejin.cn)

二十一、观察者模式(observer)

定义对象之间的一对多依赖,当一个对象状态改变时,它的所有依赖都会收到通知并且自动更新状态。

主题(Subject)是被观察的对象,而其所有依赖者(Observer)称为观察者。

img

  • 主题(Subject)具有注册和移除观察者、并通知所有观察者的功能,主题是通过维护一张观察者列表来实现这些操作的。

  • 观察者(Observer)的注册功能需要调用主题的 registerObserver() 方法,观察者为所有的具体观察者定义一个接口,在得到主题的通知时更新自己

  • ConcreteSubject类是具体主题,将有关状态存入具体观察者对象,在具体主题内部状态改变时,给所有登记过的观察者发出通知;

  • ConcreteObserver是具体观察者,实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协同。

Implementation

主题Subject

  首先定义一个观察者数组,并实现增、删及通知操作。它的职责很简单,就是定义谁能观察,谁不能观察,用Vector是线程同步的,比较安全,也可以使用ArrayList,是线程异步的,但不安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Subject {

//观察者数组
private Vector<Observer> oVector = new Vector<>();

//增加一个观察者
public void addObserver(Observer observer) {
this.oVector.add(observer);
}

//删除一个观察者
public void deleteObserver(Observer observer) {
this.oVector.remove(observer);
}

//通知所有观察者
public void notifyObserver() {
for(Observer observer : this.oVector) {
observer.update();
}
}

}

抽象观察者Observer

  观察者一般是一个接口,每一个实现该接口的实现类都是具体观察者。

1
2
3
4
public interface Observer {
//更新
public void update();
}

具体主题

  继承Subject类,在这里实现具体业务,在具体项目中,该类会有很多变种。

1
2
3
4
5
6
7
8
9
public class ConcreteSubject extends Subject {

//具体业务
public void doSomething() {
//...
super.notifyObserver();
}

}

具体观察者

  实现Observer接口。

1
2
3
4
5
6
7
8
public class ConcreteObserver implements Observer {

@Override
public void update() {
System.out.println("收到消息,进行处理");
}

}

Client客户端

  首先创建一个被观察者,然后定义一个观察者,将该被观察者添加到该观察者的观察者数组中,进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Client {

public static void main(String[] args) {
//创建一个主题
ConcreteSubject subject = new ConcreteSubject();
//定义一个观察者
Observer observer = new ConcreteObserver();
//观察
subject.addObserver(observer);
//开始活动
subject.doSomething();
}

}

开发中常见的场景

  • 聊天室程序的,服务器转发给所有客户端

  • 网络游戏(多人联机对战)场景中,服务器将客户端的状态进行分发

  • 邮件订阅

  • Servlet中,监听器的实现

  • Android中,广播机制

  • JDK的AWT中事件处理模型,基于观察者模式的委派事件模型(Delegation Event Model)

    • 事件源—————-目标对象

    • 事件监听器————观察者

  • 京东商城中,群发某商品打折信息

观察者模式的应用

  1. 何时使用

  • 一个对象状态改变,所有的依赖对象都将得到通知

 2. 方法

  • 使用面向对象技术

 3. 优点

  • 观察者和被观察者是抽象耦合的
  • 建立了一套触发机制

 4. 缺点

  • 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间
  • 如果观察者和观察目标间有循环依赖,可能导致系统崩溃
  • 没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的

 5. 使用场景

  • 关联行为场景
  • 事件多级触发场景
  • 跨系统的消息变换场景,如消息队列的处理机制

  6. 应用实例

  • 手机丢了,委托别人给其他人发消息通知
  • 通知老师/老板来了
  • 拍卖,拍卖师观察最高标价,然后通知给其它竞价者竞价
  • 在一个目录下建立一个文件,会同时通知目录管理器增加目录,并通知磁盘减少空间,文件是被观察者,目录管理器和磁盘管理器是观察者
  • 猫叫了一声,吓着了老鼠,也惊到了主人,猫是被观察者,老鼠和人是观察者

  7. 注意事项

  • 避免循环引用
  • 如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式

二十二、备忘录模式(Memento)

在不违反封装的情况下获得对象的内部状态,从而在需要时可以将对象恢复到最初状态。

img

  • Originator:原始对象
  • Caretaker:负责保存好备忘录
  • Memento:备忘录,存储原始对象的状态。备忘录实际上有两个接口,一个是提供给 Caretaker 的窄接口:它只能将备忘录传递给其它对象;一个是提供给 Originator 的宽接口,允许它访问到先前状态所需的所有数据。理想情况是只允许 Originator 访问本备忘录的内部状态。

Implementation

发起人角色

  记录当前时刻的内部状态,并负责创建和恢复备忘录数据,允许访问返回到先前状态所需的所有数据。

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
public class Originator {

private String state;

public String getState() {
return state;
}

public void setState(String state) {
this.state = state;
}

public Memento createMento() {
return (new Memento(state));
}

public void setMemento(Memento memento) {
state = memento.getState();
}

public void show() {
System.out.println("state = " + state);
}

}

备忘录角色

  负责存储Originator发起人对象的内部状态,在需要的时候提供发起人需要的内部状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Memento {

private String state;

public Memento(String state) {
this.state = state;
}

public String getState() {
return state;
}

}

备忘录管理员角色

  对备忘录进行管理、保存和提供备忘录,只能将备忘录传递给其他角色。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Caretaker {

private Memento memento;

public Memento getMemento() {
return memento;
}

public void setMemento(Memento memento) {
this.memento = memento;
}

}

Client客户端

  下面编写一小段代码测试一下,即先将状态置为On,保存后再将状态置为Off,然后通过备忘录管理员角色恢复初始状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Client {

public static void main(String[] args) {
Originator originator = new Originator();
originator.setState("On"); //Originator初始状态
originator.show();

Caretaker caretaker = new Caretaker();
caretaker.setMemento(originator.createMento());

originator.setState("Off"); //Originator状态变为Off
originator.show();

originator.setMemento(caretaker.getMemento()); //回复初始状态
originator.show();
}

}

备忘录模式的应用

  1. 何时使用

  • 需要记录一个对象的内部状态时,为了允许用户取消不确定或者错误的操作,能够恢复到原先的状态

  2. 方法

  • 通过一个备忘录类专门存储对象状态

  3. 优点

  • 给用户提供了一种可以恢复状态的机制,可以使用能够比较方便地回到某个历史的状态
  • 实现了信息的封装,使得用户不需要关心状态的保存细节

  4. 缺点

  • 消耗资源

  5. 使用场景

  • 需要保存和恢复数据的相关场景
  • 提供一个可回滚的操作,如ctrl+z、浏览器回退按钮、Backspace键等
  • 需要监控的副本场景

  6. 应用实例

  • 游戏存档
  • ctrl+z键、浏览器回退键等(撤销/还原)
  • 棋盘类游戏的悔棋
  • 数据库事务的回滚

  7. 注意事项

  • 为了符合迪米特法则,需要有一个管理备忘录的类
  • 不要在频繁建立备份的场景中使用备忘录模式。为了节约内存,可使用原型模式+备忘录模式

十四、中介者模式(Mediator)

集中相关对象之间复杂的沟通和控制方式。

img

  • Mediator是抽象中介者,定义了同事对象到中介者对象的接口;
  • Colleague是抽象同事类;
  • ConcreteMediator是具体中介者对象,实现抽象类的方法,它需要知道所有具体同事类,并从具体同事接收消息,向具体同事对象发出命令;
  • ConcreteColleague是具体同事类,每个具体同事只知道自己的行为,而不了解其它同事类的情况,但它们却都认识中介者对象。

Implementation

抽象中介者

  抽象中介者角色定义统一的接口,用于各同事角色之间的通信。

1
2
3
4
5
6
public abstract class Mediator {

//抽象的发送消息方法
public abstract void send(String message, Colleague colleague);

}

抽象同事类

  每一个同事角色都知道中介者角色,而且与其它的同事角色通信的时候,一定要通过中介者角色协作。每个同事类的行为分两种:一种是同事本身行为,比如改变对象本身的状态,处理自己的行为等,这种行为叫做自发行为,与其它同事类或者中介者没有任何依赖;第二种是必须依赖中介者才能完成的行为,叫做依赖方法。

1
2
3
4
5
6
7
8
9
public abstract class Colleague {

protected Mediator mediator;

public Colleague(Mediator mediator) {
this.mediator = mediator;
}

}

具体中介者类

  具体中介者角色通过协调各同事角色实现协作行为,因此它必须依赖于各个同事角色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ConcreteMediator extends Mediator {

private ConcreteColleague1 colleague1;
private ConcreteColleague2 colleague2;

public void setColleague1(ConcreteColleague1 colleague1) {
this.colleague1 = colleague1;
}

public void setColleague2(ConcreteColleague2 colleague2) {
this.colleague2 = colleague2;
}

@Override
public void send(String message, Colleague colleague) {
if(colleague == colleague1) {
colleague2.notify(message);
} else {
colleague1.notify(message);
}
}

}

具体同事类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConcreteColleague1 extends Colleague {

public ConcreteColleague1(Mediator mediator) {
super(mediator);
}

public void send(String message) {
mediator.send(message, this);
}

public void notify(String message) {
System.out.println("同事1得到消息:" + message);
}

}

Client客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {

public static void main(String[] args) {
ConcreteMediator mediator = new ConcreteMediator();

ConcreteColleague1 colleague1 = new ConcreteColleague1(mediator);
ConcreteColleague2 colleague2 = new ConcreteColleague2(mediator);

mediator.setColleague1(colleague1);
mediator.setColleague2(colleague2);

colleague1.send("Nice to meet u.");
colleague2.send("Nice to meet u too.");
}

}

中介者模式的应用

  1. 何时使用

  • 多个类相互耦合,形成网状结构时

  2. 方法

  • 将网状结构分离为星型结构

  3. 优点

  • 减少类间依赖,降低了耦合
  • 符合迪米特原则

  4. 缺点

  • 中介者会膨胀的很大,而且逻辑复杂

  5. 使用场景

  • 系统中对象之间存在比较复杂的引用关系
  • 想通过一个中间类来封装多个类的行为,而又不想生成太多的子类

开发中常见的场景

  • MVC模式(其中的C,控制器就是一个中介者对象。M和V都和他打交道)
  • 窗口游戏程序,窗口软件开发中窗口对象也是一个中介者对象
  • 图形界面开发GUI中,多个组件之间的交互,可以通过引入一个中介者 对象来解决,可以是整体的窗口对象或者DOM对象
  • Java.lang.reflect.Method#invoke()

十五、命令模式(command)

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。

img

  • Invoker调用者/请求者:请求的发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联。在程序运行时,将调用命令对象的execute(),间接调用接收者的相关操作。
  • Command是命令角色,需要执行的所有命令都在这里声明,可以是接口或抽象类;
  • Receiver接收者:知道如何实施与执行一个请求相关的操作,任何类都可能作为一个接收者;
  • ConcreteCommand将一个接收者对象绑定与一个动作,调用接收者相应的操作,以实现Execute。
  • Client:在客户类中故需要创建调用者对象、具体命令类对象,在创建具体命令对象是指定对应的接收者。发送者和接收者之间没有直接的关系,都通过命令对象来调用

Implementation

command类

​ 用来声明执行操作的接口/抽象类

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class Command {

protected Receiver receiver;

public Command(Receiver receiver) {
this.receiver = receiver;
}

//执行命令的方法
abstract public void execute();

}

ConcreteCommand类

  具体的Command类,用于构造传递接收者,根据环境需求,具体的命令类也可能有n个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ConcreteCommand extends Command {

//构造传递接收者
public ConcreteCommand(Receiver receiver) {
super(receiver);
}

//必须实现一个命令
@Override
public void execute() {
receiver.action();
}

}

Invoker类

  接收命令,并执行命令。

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

private Command command;

//接受命令
public void setCommand(Command command) {
this.command = command;
}

//执行命令
public void executeCommand() {
command.execute();
}

}

Receiver类

  该角色就是干活的角色, 命令传递到这里是应该被执行的。

1
2
3
4
5
6
7
public class Receiver {

public void action() {
System.out.println("执行请求!");
}

}

Client类

  首先定义一个接收者,然后定义一个命令用于发送给接收者,之后再声明一个调用者,即可把命令交给调用者执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {

public static void main(String[] args) {
//定义接收者
Receiver receiver = new Receiver();
//定义一个发送给接收者的命令
Command command = new ConcreteCommand(receiver);
//声明调用者
Invoker invoker = new Invoker();

//把命令交给调用者执行
invoker.setCommand(command);
invoker.executeCommand();
}

}

命令模式的应用

  1. 何时使用

  • 在某些场合,如要对行为进行“记录、撤销/重做、事务”等处理时

 2. 方法

  • 通过调用者调用接收者执行命令,顺序为调用者→接收者→命令

 3. 优点

  • 类间耦合,调用者角色与接收者角色之间没有任何依赖关系
  • 可扩展性
  • 命令模式结合职责链模式可以实现命令族解析任务;结合模板方法模式可以减少Command子类的膨胀问题

 4. 缺点

  • 可能导致某些系统有过多的具体命令类

 5. 使用场景

  • 认为是命令的地方都可以使用
  • 系统需要支持命令的撤销/恢复操作时

 6. 应用实例

  • GUI中每一个按钮都是一条命令
  • 模拟CMD(DOS命令)
  • 订单的撤销/恢复
  • 触发-反馈机制的处理

十六、解释器模式(interpreter)——不常用

为语言创建解释器,通常由语言的语法和语法分析来定义。

  • 用于描述如何构成一个简单的语言解释器,主要用于使用面向对象语言开发的 编译器和解释器设计。
  • 当我们需要开发一种新的语言时,可以考虑使用解释器模式。
  • 尽量不要使用解释器模式,后期维护会有很大麻烦。在项目中,可以使用 Jruby,Groovy、java的js引擎来替代解释器的作用,弥补java语言的不足

十七、访问者模式(visitor)

表示一个作用于某对象结构中的各元素的操作,它使我们可以在不改变元素的类的前提下定义作用于这些元素的新操作。

开发中的场景(应用范围非常窄,了解即可)

  • XML文档解析器设计
  • 编译器的设计
  • 复杂集合对象的处理

十八、策略模式(strategy)

定义一系列算法,封装每个算法,并使它们可以互换。(分离算法,选择实现

策略模式可以让算法独立于使用它的客户端。

img

  • Context是上下文,用一个ConcreteStrategy来配置,维护一个对Strategy对象的引用;
  • Strategy是策略类,用于定义所有支持算法的公共接口;
  • ConcreteStrategy是具体策略类,封装了具体的算法或行为,继承于Strategy。

与状态模式的比较

状态模式的类图和策略模式类似,并且都是能够动态改变对象的行为。但是状态模式是通过状态转移来改变 Context 所组合的 State 对象,而策略模式是通过 Context 本身的决策来改变组合的 Strategy 对象。所谓的状态转移,是指 Context 在运行过程中由于一些条件发生改变而使得 State 对象发生改变,注意必须要是在运行过程中。

状态模式主要是用来解决状态转移的问题,当状态发生转移了,那么 Context 对象就会改变它的行为;而策略模式主要是用来封装一组可以互相替代的算法族,并且可以根据需要动态地去替换 Context 使用的算法。

Implementation

Context上下文

  Context上下文角色,也叫Context封装角色,起承上启下的作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Context {

Strategy strategy;

public Context(Strategy strategy) {
this.strategy = strategy;
}

//上下文接口
public void contextInterface() {
strategy.algorithmInterface();
}

}

策略角色

  抽象策略角色,是对策略、算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性。algorithm是“运算法则”的意思。

1
2
3
4
5
6
public abstract class Strategy {

//算法方法
public abstract void algorithmInterface();

}

具体策略角色

  用于实现抽象策略中的操作,即实现具体的算法。

1
2
3
4
5
6
7
8
public class ConcreteStrategyA extends Strategy {

@Override
public void algorithmInterface() {
System.out.println("算法A实现");
}

}

Client客户端

  下面依次更换策略,测试一下策略模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {

public static void main(String[] args) {
Context context;

context = new Context(new ConcreteStrategyA());
context.contextInterface();

context = new Context(new ConcreteStrategyB());
context.contextInterface();

context = new Context(new ConcreteStrategyC());
context.contextInterface();
}

}

策略模式的应用

  1. 何时使用

  • 一个系统有许多类,而区分它们的只是他们直接的行为时

  2. 方法

  • 将这些算法封装成一个一个的类,任意的替换

  3. 优点

  • 算法可以自由切换
  • 避免使用多重条件判断(如果不用策略模式我们可能会使用多重条件语句,不利于维护)
  • 扩展性良好,增加一个策略只需实现接口即可

  4. 缺点

  • 策略类数量会增多,每个策略都是一个类,复用的可能性很小
  • 所有的策略类都需要对外暴露

  5. 使用场景

  • 多个类只有算法或行为上稍有不同的场景
  • 算法需要自由切换的场景
  • 需要屏蔽算法规则的场景

  6. 应用实例

  • 出行方式,自行车、汽车等,每一种出行方式都是一个策略
  • 商场促销方式,打折、满减等
  • Java AWT中的LayoutManager,即布局管理器

  7. 注意事项

  • 如果一个系统的策略多于四个,就需要考虑使用混合模式来解决策略类膨胀的问题

开发中常见的场景

  • JAVASE中GUI编程中,布局管理
  • Spring框架中,Resource接口,资源访问策略
  • javax.servlet.http.HttpServlet#service()

十九、模板方法模式(template method)

定义算法框架,并将一些步骤的实现延迟到子类。

通过模板方法,子类可以重新定义算法的某些步骤,而不用改变算法的结构。

处理步骤父类中定义好,具体实现延迟到子类中定义。

img

  • AbstractClass实现类一个模板方法,定义了算法的骨架,具体子类将重定义PrimitiveOperation以实现一个算法的步骤;
  • ConcreteClass实现了PrimitiveOperation以完成算法中与特定子类相关的步骤。

Implementation

抽象模板类

  定义一个模板方法来组合PrimitiveOperation1()和PrimitiveOperation2()两个方法形成一个算法,然后让子类重定义这两个方法。

1
2
3
4
5
6
7
8
9
10
11
public abstract class AbstractClass {

public abstract void PrimitiveOperation1();
public abstract void PrimitiveOperation2();

public void TemplateMethod() {
PrimitiveOperation1();
PrimitiveOperation2();
}

}

具体模板类

  这里定义两个具体模板类,ConcreteClassA及ConcreteClassB来进行测试,继承抽象模板类,实现具体方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ConcreteClassA extends AbstractClass {

@Override
public void PrimitiveOperation1() {
System.out.println("具体方法A方法1实现");
}

@Override
public void PrimitiveOperation2() {
System.out.println("具体方法A方法2实现");
}

}

Client客户端

  通过调用模板方法来分别得到不同的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {

public static void main(String[] args) {
AbstractClass abstractClass;

abstractClass = new ConcreteClassA();
abstractClass.TemplateMethod();

abstractClass = new ConcreteClassB();
abstractClass.TemplateMethod();
}

}

开发中常见的场景:

非常频繁,各个框架、类库都存在

  • 数据库访问的封装
  • Junit单元测试
  • servlet中关于doGet()和doPost()方法调用
  • Hibernate中模板程序
  • spring中JDBCTemplate、HibernateTemplate···

模板方法模式的应用

  1. 何时使用

  • 有一些通用的方法时

  2. 方法

  • 将通用算法抽象出来

  3. 优点

  • 封装不变部分,扩展可变部分
  • 提取公共部分代码,便于维护
  • 行为由父类控制,子类实现

  4. 缺点

  • 每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大

  5. 使用场景

  • 有多个子类共有的方法,且逻辑相同
  • 重要的、复杂的方法,可以考虑作为模板方法
  • 重构时,模板方法模式是一个经常使用到的模式,把相同的代码抽取到父类中,通过钩子函数约束其行为

  6. 应用实例

  • 做试卷,大家题目都是一样的,只是答案不同
  • 对于汽车,车从发动到停车的顺序是相同的,不同的是引擎声、鸣笛声等
  • 造房时,地基、走线、水管都一样,只有在建筑后期才有差异

  7. 注意事项

  • 为防恶意操作,一般模板方法都加上final关键字

二十、状态模式(state)

允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它所属的类。

img

  • Context类为环境角色,用于维护一个ConcreteState子类的实例,这个实例定义当前的状态;
  • State为抽象状态角色,定义一个接口以封装与Context的一个特定接口状态相关的行为;
  • ConcreteState是具体状态角色,每一个子类实现一个与Context的一个状态相关的行为。

Implementation

Context类

  环境角色具有两个职责,即处理本状态必须完成的任务,及决定是否可以过渡到其它状态。对于环境角色,有几个不成文的约束:

  • 即把状态对象声明为静态常量,有几个状态对象就声明几个状态常量
  • 环境角色具有状态抽象角色定义的所有行为,具体执行使用委托方式
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
public class Context {

//定义状态
public final static State STATE1 = new ConcreteState1();
public final static State STATE2 = new ConcreteState2();

//当前状态
private State currentState;

//获得当前状态
public State getCurrentState() {
return currentState;
}

//设置当前状态
public void setCurrentState(State currentState) {
this.currentState = currentState;
// System.out.println("当前状态:" + currentState);
//切换状态
this.currentState.setContext(this);
}

public void handle1() {
this.currentState.handle1();
}
public void handle2() {
this.currentState.handle2();
}

}

State抽象状态类

  抽象环境中声明一个环境角色,提供各个状态类自行访问,并且提供所有状态的抽象行为,由各个实现类实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class State {

protected Context context;
public void setContext(Context context) {
this.context = context;
}

//行为1
public abstract void handle1();
//行为2
public abstract void handle2();

}

具体状态

  具体状态实现,这里以定义ConcreteState1和ConcreteState2两个具体状态类为例,ConcreteState2的具体内容同ConcreteState1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConcreteState1 extends State {

@Override
public void handle1() {
//...
System.out.println("ConcreteState1 的 handle1 方法");
}

@Override
public void handle2() {
super.context.setCurrentState(Context.STATE2);
System.out.println("ConcreteState1 的 handle2 方法");
}

}

Client客户端

  定义Context环境角色,初始化具体状态1,执行行为观察结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {

public static void main(String[] args) {
//定义环境角色
Context context = new Context();
//初始化状态
context.setCurrentState(new ConcreteState1());
//行为执行
context.handle1();
context.handle2();
}

}

开发中常见的场景

  • 银行系统中账号状态的管理
  • OA系统中公文状态的管理
  • 酒店系统中,房间状态的管理
  • 线程对象各状态之间的切换

九、装饰模式(decorator)

为对象动态添加功能。

装饰模式是一种用来代替继承的技术,无须通过继承增加子类就能拓展对象的新功能。使用对象的关联关系代替继承关系,更加灵活,同时可以避免类型体系的快速膨胀

img

  • Component:抽象构件,真实对象和装饰对象都有的接口,这样,客户端对象可以以与真实对象相同的方式同装饰对象交互
  • ConreteComponent:具体构件对象(真实对象)
  • Decorator:装饰角色。持有一个抽象构件的引用,装饰对象接受所有客户端的请求,并把这些请求转发给真实的对象。这样,就能在真实对象调用前后增加新的功能
  • ConreteDecorator:具体装饰对象。负责给构件对象增加新的责任

要点: 装饰者与被装饰者拥有共同的超类,继承的目的是继承类型,而不是行为

Implementation

component抽象类:

Component是一个接口或是抽象类,就是定义我们最核心的对象,也就是最原始的对象。

1
2
3
4
5
public abstract class Component {

public abstract void operation();

}

conretetComponent:

具体构件,通过继承实现Component抽象类中的抽象方法。是最核心、最原始、最基本的接口或抽象类的实现,我们要装饰的就是它。

1
2
3
4
5
6
7
public class ConretetComponent extends Component
{
@Override
public void operation(){
System.out.println("具体对象的操作");
}
}

Decorator装饰类:

  一般是一个抽象类,在其属性里必然有一个private变量指向Component抽象构件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class Decorator extends Component {

private Component component = null;

//通过构造函数传递给被修饰者
public Decorator(Component component) {
this.component = component;
}

//委托给被修饰者执行
@Override
public void operation() {
if(component != null) {
this.component.operation();
}
}

}

ConcreteDecorator类:

  我们可以写多个具体实现类,把最核心的、最原始的、最基本的东西装饰成其它东西。

  这里就写两个类,稍改一下二者的实现顺序,看看结果。

  A类,它的operation()方法先执行了method1()方法,再执行了Decorator的operation()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ConcreteDecoratorA extends Decorator {

//定义被修饰者
public ConcreteDecoratorA(Component component) {
super(component);
}

//定义自己的修饰方法
private void method1() {
System.out.println("method1 修饰");
}

@Override
public void operation() {
this.method1();
super.operation();
}

}

B类,它的operation()方法先执行了Decorator的operation()方法,再执行了method2()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ConcreteDecoratorB extends Decorator {

//定义被修饰者
public ConcreteDecoratorB(Component component) {
super(component);
}

//定义自己的修饰方法
private void method2() {
System.out.println("method2 修饰");
}

@Override
public void operation() {
super.operation();
this.method2();
}

}

Client客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {

public static void main(String[] args) {
Component component = new ConcreteComponent();
//第一次修饰
component = new ConcreteDecoratorA(component);
//第二次修饰
component = new ConcreteDecoratorB(component);
//修饰后运行
component.operation();
}

}

IO流实现细节

  • Component抽象构件角色:
    • io流中的InputStream、OutputStream、Reader、Writer
  • ConcreteComponent 具体构件角色:
    • io流中的FileInputStream、FileOutputStream
  • Decorator装饰角色:
    • 持有一个抽象构件的引用:io流中的FilterInputStream、FilterOutputStream
  • ConcreteDecorator具体装饰角色:
    • 负责给构件对象增加新的责任。Io流中的BufferedOutputStream、BufferedInputStream等。

装饰模式的应用

 1. 何时使用

  • 在不想增加很多子类的情况下扩展类时

 2. 方法

  • 将具体功能职责划分,同时继承装饰者模式

 3. 优点

  • 装饰类和被装饰类可以独立发展,而不会相互耦合。它有效地把类的核心职责和装饰功能分开了
  • 装饰模式是继承关系的一个替代方案
  • 装饰模式可以动态地扩展一个实现类的功能

  4. 缺点

  • 多层装饰比较复杂。比如我们现在有很多层装饰,出了问题,一层一层检查,最后发现是最里层的装饰出问题了,想想工作量都害怕

  5. 使用场景

  • 需要扩展一个类的功能时
  • 需要动态地给一个对象增加功能,并可以动态地撤销时
  • 需要为一批的兄弟类进行改装或加装功能时

装饰模式和桥接模式的区别

两个模式都是为了解决过多子类对象问题。但他们的诱因不一样。桥接模式是对象自身现有机制沿着多个维度变化,是既有部分不稳定。装饰模式是为了增加新的功能。

参考文章

简说设计模式——装饰模式 - JAdam - 博客园 (cnblogs.com)

十、外观模式(facade)

提供了一个统一的接口,用来访问子系统中的一群接口,从而让子系统更容易使用。

Implementation

观看电影需要操作很多电器,使用外观模式实现一键看电影功能。

子系统角色:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SubSystem {
public void turnOnTV() {
System.out.println("turnOnTV()");
}

public void setCD(String cd) {
System.out.println("setCD( " + cd + " )");
}

public void startWatching(){
System.out.println("startWatching()");
}
}

外观类:

1
2
3
4
5
6
7
8
9
public class Facade {
private SubSystem subSystem = new SubSystem();

public void watchMovie() {
subSystem.turnOnTV();
subSystem.setCD("a movie");
subSystem.startWatching();
}
}

客户端:

1
2
3
4
5
6
public class Client {
public static void main(String[] args) {
Facade facade = new Facade();
facade.watchMovie();
}
}

设计原则

最少知识原则:只和你的密友谈话。也就是说客户对象所需要交互的对象应当尽可能少。

外观模式的应用

  1. 何时使用

  • 客户端不需要知道系统内部的复杂联系,整个系统只提供一个“接待员”即可
  • 定义系统的入口

 2. 方法

  • 客户端不与系统耦合,外观类与系统耦合

 3. 优点

  • 减少了系统的相互依赖
  • 提高了灵活性。不管系统内部如何变化,只要不影响到外观对象,任你自由活动
  • 提高了安全性。想让你访问子系统的哪些业务就开通哪些逻辑,不在外观上开通的方法,你就访问不到

 4. 缺点

  • 不符合开不原则,修改很麻烦

 5. 使用场景

  • 为一个复杂的模块或子系统提供一个外界访问的接口
  • 子系统相对独立,外界对子系统的访问只要黑箱操作即可
  • 预防低水平人员带来的风险扩散

十一、享元模式(FlyWeight)

运用共享技术有效地支持大量细粒度的对象。

池技术:String常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式。

img

  • FlyWeightFactory:享元工厂类。创建并管理享元对象,享元池一般设计成键值对。
  • FlyWeight:抽象享元类。通常是一个接口或抽象类,声明公共方法,这些方法可以向外界提供对象的内部状态,设置外部状态。
  • ConcreteFlyWeight:具体享元类。为内部状态提供成员变量进行存储
  • UnsharedConcreteFlyWeight:非共享享元类。不能被共享的子类可以设计为非共享享元类

Implementation

Flyweight抽象类

  所有具体享元类的超类或接口,通过这个接口,Flyweight可以接受并作用于外部状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class Flyweight {

//内部状态
public String intrinsic;
//外部状态
protected final String extrinsic;

//要求享元角色必须接受外部状态
public Flyweight(String extrinsic) {
this.extrinsic = extrinsic;
}

//定义业务操作
public abstract void operate(int extrinsic);

public String getIntrinsic() {
return intrinsic;
}

public void setIntrinsic(String intrinsic) {
this.intrinsic = intrinsic;
}

}

ConcreteFlyweight类

  继承Flyweight超类或实现Flyweight接口,并为其内部状态增加存储空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ConcreteFlyweight extends Flyweight {

//接受外部状态
public ConcreteFlyweight(String extrinsic) {
super(extrinsic);
}

//根据外部状态进行逻辑处理
@Override
public void operate(int extrinsic) {
System.out.println("具体Flyweight:" + extrinsic);
}

}

UnsharedConcreteFlyweight类

  指那些不需要共享的Flyweight子类。

1
2
3
4
5
6
7
8
9
10
11
12
public class UnsharedConcreteFlyweight extends Flyweight {

public UnsharedConcreteFlyweight(String extrinsic) {
super(extrinsic);
}

@Override
public void operate(int extrinsic) {
System.out.println("不共享的具体Flyweight:" + extrinsic);
}

}

FlyweightFactory类

  一个享元工厂,用来创建并管理Flyweight对象,主要是用来确保合理地共享Flyweight,当用户请求一个Flyweight时,FlyweightFactory对象提供一个已创建的实例或创建一个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class FlyweightFactory {

//定义一个池容器
private static HashMap<String, Flyweight> pool = new HashMap<>();

//享元工厂
public static Flyweight getFlyweight(String extrinsic) {
Flyweight flyweight = null;

if(pool.containsKey(extrinsic)) { //池中有该对象
flyweight = pool.get(extrinsic);
System.out.print("已有 " + extrinsic + " 直接从池中取---->");
} else {
//根据外部状态创建享元对象
flyweight = new ConcreteFlyweight(extrinsic);
//放入池中
pool.put(extrinsic, flyweight);
System.out.print("创建 " + extrinsic + " 并从池中取出---->");
}

return flyweight;
}
}

Client客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Client {

public static void main(String[] args) {
int extrinsic = 22;

Flyweight flyweightX = FlyweightFactory.getFlyweight("X");
flyweightX.operate(++ extrinsic);

Flyweight flyweightY = FlyweightFactory.getFlyweight("Y");
flyweightY.operate(++ extrinsic);

Flyweight flyweightZ = FlyweightFactory.getFlyweight("Z");
flyweightZ.operate(++ extrinsic);

Flyweight flyweightReX = FlyweightFactory.getFlyweight("X");
flyweightReX.operate(++ extrinsic);

Flyweight unsharedFlyweight = new UnsharedConcreteFlyweight("X");
unsharedFlyweight.operate(++ extrinsic);
}

}

内部状态和外部状态

  上面享元模式的定义为我们提出了两个要求:细粒度和共享对象。我们知道分配太多的对象到应用程序中将有损程序的性能,同时还容易造成内存溢出,要避免这种情况,用到的就是共享技术,这里就需要提到内部状态和外部状态了。

  因为要求细粒度对象,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。

  内部状态指对象共享出来的信息,存储在享元对象内部并且不会随环境的改变而改变;外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态。

  我们举一个最简单的例子,棋牌类游戏大家都有玩过吧,比如说说围棋和跳棋,它们都有大量的棋子对象,围棋和五子棋只有黑白两色,跳棋颜色略多一点,但也是不太变化的,所以棋子颜色就是棋子的内部状态;而各个棋子之间的差别就是位置的不同,我们落子嘛,落子颜色是定的,但位置是变化的,所以方位坐标就是棋子的外部状态。

  那么为什么这里要用享元模式呢?可以想象一下,上面提到的棋类游戏的例子,比如围棋,理论上有361个空位可以放棋子,常规情况下每盘棋都有可能有两三百个棋子对象产生,因为内存空间有限,一台服务器很难支持更多的玩家玩围棋游戏,如果用享元模式来处理棋子,那么棋子对象就可以减少到只有两个实例,这样就很好的解决了对象的开销问题。

享元模式的应用

  1. 何时使用

  • 系统中有大量对象时
  • 这些对象消耗大量内存时
  • 这些对象的状态大部分可以外部化时

  2. 方法

  • 用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用HashMap/HashTable存储

  3. 优点

  • 大大减少了对象的创建,降低了程序内存的占用,提高效率

  4. 缺点

  • 提高了系统的复杂度。需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变
  • 为了节省内存,共享了内部状态,分离出外部状态,而读取外部状态 使运行时间变长。用时间换取了空间。

 5. 使用场景

  • 系统中存在大量相似对象
  • 需要缓冲池的场景

 6. 应用实例

  • String常量池
  • 数据库连接池

 7. 注意事项

  • 注意划分内部状态和外部状态,否则可能会引起线程安全问题
  • 这些类必须有一个工厂类加以控制

结构型模式汇总

代理模式 为真实对象提供一个代理,从而控制对真实对象的访问
适配模式 使原本由于接口不兼容不能一起工作的类可以一起工作
桥接模式 处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立的继承结构,使各个维度可以独立的扩展在抽象层建立关联。
组合模式 将对象组合成树状结构以表示”部分和整体”层次结构,使得客户可以统一 的调用叶子对象和容器对象
装饰模式 动态地给一个对象添加额外的功能,比继承灵活
外观模式 为子系统提供统一的调用接口,使得子系统更加容易使用
享元模式 运用共享技术有效的实现管理大量细粒度对象,节省内存,提高效率

行为型模式

  • 行为型模式关注的是系统中对象之间的相互交互,研究系统在运行时对象之间的相互通信和协作,进一步明确对象的职责。

  • 创建型模式关注的是对象的创建过程。

  • 结构型模式关注的是对象和类的组织。

十二、责任链模式(chain of responsibility)

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链发送该请求,直到有一个对象处理它为止。

img

  • Handler:抽象处理者。定义了一个处理请求的接口。
  • ConcreteHandler:具体处理者。处理它所负责的请求,可访问它的后继者。如果可处理该请求就处理,否则就将该请求转发给它的后继者。

Implementation

抽象处理者

  抽象处理者实现了三个职责:

  • 定义一个请求的处理方法handlerMessage(),是唯一对外开放的方法
  • 定义一个链的编排方式setNext(),用于设置下一个处理者
  • 定义了具体的请求者必须实现的两个方法,即定义自己能够处理的级别的getHandlerLevel()方法及具体的处理任务echo()方法
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
public abstract class Handler {

private Handler nextHandler; //下一个处理者

public final Response handlerMessage(Request request) {
Response response = null;

if(this.getHandlerLevel().equals(request.getRequestLevel())) { //判断是否是自己的处理级别
response = this.echo(request);
} else {
if(this.nextHandler != null) { //下一处理者不为空
response = this.nextHandler.handlerMessage(request);
} else {
//没有适当的处理者,业务自行处理
}
}

return response;
}

//设定下一个处理者
public void setNext(Handler handler) {
this.nextHandler = handler;
}

//每个处理者的处理等级
protected abstract Level getHandlerLevel();

//每个处理者都必须实现的处理任务
protected abstract Response echo(Request request);

}

具体处理者

  这里我们定义三个具体处理者,以便能组成一条链,ConcreteHandlerB及ConcreteHandlerC就不再赘述了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConcreteHandlerA extends Handler {

@Override
protected Level getHandlerLevel() {
//设置自己的处理级别
return null;
}

@Override
protected Response echo(Request request) {
//完成处理逻辑
return null;
}

}

责任链模式的应用

开发中常见的场景:

  • Java中,异常机制就是一种责任链模式。一个try可以对应多个catch, 当第一个catch不匹配类型,则自动跳到第二个catch.
  • Javascript语言中,事件的冒泡和捕获机制。Java语言中,事件的处理 采用观察者模式。
  • Servlet开发中,过滤器的链式处理
  • Struts2中,拦截器的调用也是典型的责任链模式

  1. 何时使用

  • 处理消息时

  2. 方法

  • 拦截的类都实现同一接口

  3. 优点

  • 将请求和处理分开,实现解耦,提高系统的灵活性
  • 简化了对象,使对象不需要知道链的结构

  4. 缺点

  • 性能会收到影响,特别是在链比较长的时候
  • 调试不方便。采用了类似递归的方式,调试时逻辑可能比较复杂
  • 不能保证请求一定被接收

  5. 使用场景

  • 有多个对象可以处理同一个请求
  • 在不明确指定接收者的情况下,向多个对象中的提交请求
  • 可动态指定一组对象处理请求

  6. 应用实例

  • 多级请求
  • 击鼓传花
  • 请假/加薪请求
  • Java Web中Tomcat对Encoding的处理、拦截器

  7. 注意事项

  • 需控制链中最大节点数量,一般通过在Handler中设置一个最大节点数量,在setNext()方法中判断是否已经超过阀值,超过则不允许该链建立,避免出现超长链无意识地破坏系统性能

十三、迭代器模式(iterator)

提供一种顺序访问聚合对象元素的方法,并且不暴露聚合对象的内部表示。

img

  • Aggregate:聚集抽象类。负责提供创建具体迭代器角色的接口
  • Iterator:迭代抽象类,用于定义得到开始对象、得到下一个对象、判断是否到结尾、当前对象等抽象方法,统一接口
  • ConcreteAggregate:具体聚集类,继承Aggregate
  • ConcreteIterator:具体迭代器类,继承Iterator,实现开始、下一个、是否结尾、当前对象等方法

Implementation

抽象容器

  负责提供接口,比如存在一个类似createIterator()这样的方法,在Java中一般是iterator()方法。

1
2
3
4
5
6
7
8
9
public interface Aggregate {

public void add(Object object);

public void remove(Object object);

public Iterator iterator();

}

抽象迭代器

  负责定义访问和遍历元素的接口,基本上有固定的三个方法,即first()获取第一个元素、next()访问下一个元素、hasNext()是否已经遍历到底部。

1
2
3
4
5
6
7
8
9
public interface Iterator {

public Object next(); //遍历到下一个元素

public boolean hasNext(); //是否已经遍历到尾部

public boolean remove(); //删除当前指向的元素

}

具体容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ConcreteAggregate implements Aggregate {

private Vector vector = new Vector();

@Override
public void add(Object object) {
this.vector.add(object);
}

public void remove(Object object) {
this.remove(object);
}

@Override
public Iterator iterator() {
return new ConcreteIterator(this.vector);
}

}

具体迭代器

 简单的实现就是通过一个游标,在一个容器中上下翻滚,遍历所有它需要查看的元素。

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
public class ConcreteIterator implements Iterator {

private Vector vector = new Vector();
public int cursor = 0; //定义当前游标

public ConcreteIterator(Vector vector) {
this.vector = vector;
}

@Override
public Object next() {
Object result = null;

if (this.hasNext()) {
result = this.vector.get(this.cursor ++);
} else {
result = null;
}

return result;
}

@Override
public boolean hasNext() {
if (this.cursor == this.vector.size()) {
return false;
}

return true;
}

@Override
public boolean remove() {
this.vector.remove(this.cursor);

return true;
}

}

迭代器模式的应用

  1. 何时使用

  • 遍历一个聚合对象时

  2. 方法

  • 把在元素间游走的责任交给迭代器,而不是聚合对象

  3. 优点

  • 支持以不同的方式遍历一个聚合对象
  • 迭代器简化了聚合类
  • 在同一个聚合上可以有多个遍历
  • 增加新的聚合类和迭代器类都很方便,无需修改原有代码

  4. 缺点

  • 增加了系统的复杂性。因为迭代器模式将存储数据和遍历数据的职责分离,增加了新的聚合类需要对应增加新的迭代器类,增加了系统的复杂性。

  5. 使用场景

  • 访问一个聚合对象的内容无需暴露它的内部表示时
  • 需要为聚合对象提供多种便利方式时
  • 为遍历不同的聚合结构提供一个统一的接口

  6. 应用实例

  • Java中的Iterator迭代器
  • foreach遍历

结构型模式

核心作用:是从程序的结构上实现松耦合,从而可以扩大整体的类结构,用来解决更大的问题。

分类:

  • 适配器模式、代理模式、桥接模式、 装饰模式、组合模式、外观模式、享元模式
代理模式 为真实对象提供一个代理,从而控制对真实对象的访问
适配模式 使原本由于接口不兼容不能一起工作的类可以一起工作
桥接模式 处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立的继承结构,使各个维度可以独立的扩展在抽象层建立关联。
组合模式 将对象组合成树状结构以表示”部分和整体”层次结构,使得客户可以统一的调用叶子对象和容器对象
装饰模式 动态地给一个对象添加额外的功能,比继承灵活
外观模式 为子系统提供统一的调用接口,使得子系统更加容易使用
享元模式 运用共享技术有效的实现管理大量细粒度对象,节省内存,提高效率

五、代理模式(proxy)

控制对其它对象的访问。

核心作用:

控制对其他对象的访问。

核心角色:

  • 抽象角色:定义代理角色和真实角色的公共给对外方法

  • 真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用

    关注真正的业务逻辑!

  • 代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。

    将统一的流程控制放到代理角色中处理!

开发框架中应用场景:

  • struts2中拦截器的实现
  • 数据库连接池关闭处理
  • Hibernate中延时加载的实现
  • mybatis中实现拦截器插件
  • AspectJ的实现
  • spring中AOP的实现
    • 日志拦截
    • 声明式事务处理
  • web service
  • RMI远程方法调用

包含四类:

  • 远程代理(remote proxy):控制对源程对象不同地址空间)的访问。它负责将请求及其参数进行编码,并向不同地址空间中的对象发送已经编码的请求。

  • 虚拟代理(Virtual Proxy):根据需要创建开销很大的对象,它可以缓存实体的附加信息,以便延迟对它的访问,例如在网站加载一个很大图片时,不能马上完成,可以用虚拟代理缓存图片的大小信息,然后生成一张临时图片代替原始图片。

  • 保护代理(Protection Proxy):按权限控制对象的访问,它负责检查调用者是否具有实现一个请求所必须的访问权限。

  • 智能代理(Smart Reference):取代了简单的指针,它在访问对象时执行一些附加操作:记录对象的引用次数;当第一次引用一个对象时,将它装入内存;在访问一个实际对象前,检查是否已经锁定了它,以确保其它对象不能改变它。


例子:

以下是一个虚拟代理的实现,模拟了图片延迟加载的情况下使用与图片大小相等的临时内容去替换原始图片,直到图片加载完成才将图片显示出来。

1
2
3
public interface Image {
void showImage();
}
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
public class HighResolutionImage implements Image {

private URL imageURL;
private long startTime;
private int height;
private int width;

public int getHeight() {
return height;
}

public int getWidth() {
return width;
}

public HighResolutionImage(URL imageURL) {
this.imageURL = imageURL;
this.startTime = System.currentTimeMillis();
this.width = 600;
this.height = 600;
}

public boolean isLoad() {
// 模拟图片加载,延迟 3s 加载完成
long endTime = System.currentTimeMillis();
return endTime - startTime > 3000;
}

@Override
public void showImage() {
System.out.println("Real Image: " + imageURL);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ImageProxy implements Image {

private HighResolutionImage highResolutionImage;

public ImageProxy(HighResolutionImage highResolutionImage) {
this.highResolutionImage = highResolutionImage;
}

@Override
public void showImage() {
while (!highResolutionImage.isLoad()) {
try {
System.out.println("Temp Image: " + highResolutionImage.getWidth() + " " + highResolutionImage.getHeight());
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
highResolutionImage.showImage();
}
}
1
2
3
4
5
6
7
8
9
10
public class ImageViewer {

public static void main(String[] args) throws Exception {
String image = "http://image.jpg";
URL url = new URL(image);
HighResolutionImage highResolutionImage = new HighResolutionImage(url);
ImageProxy imageProxy = new ImageProxy(highResolutionImage);
imageProxy.showImage();
}
}

面向切面编程AOP介绍

AOP(Aspect-Oriented Programming,面向切面的编程)

  • 它是可以通过预编译方式和运行期动态代理实现在不修改源代码的情 况下给程序动态统一添加功能的一种技术。它是一种新的方法论,它是对传统OOP编程的一种补充。

常用术语:

  • 切面(Aspect):其实就是共有功能的实现。

  • 通知(Advice):是切面的具体实现。

  • 连接点(Joinpoint):就是程序在运行过程中能够插入切面的地点。

  • 切入点(Pointcut):用于定义通知应该切入到哪些连接点上。

  • 目标对象(Target):就是那些即将切入切面的对象,也就是那些被通知的对象

  • 代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。

  • 织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。

开源的AOP框架:

  • Aspect

六、适配器模式(Adapter)

把一个类接口转换成另一个用户需要的接口。

使用场景

  • 系统需要使用现有的类,而这些类的接口不符合系统的需要。
  • 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
  • 需要一个统一的输出接口,而输入端的类型不可预知。

工作中的场景

  • 经常用来做旧系统改造和升级

  • 如果我们的系统开发之后再也不需要维护,那么很多模式都是没必要的,但是不幸的是,事实却是维护一个系统的代价往往是开发一个系统的数倍。

我们学习中见过的场景

  • java.io.InputStreamReader(InputStream)
  • java.io.OutputStreamWriter(OutputStream)

类适配器模式

一句话描述:Adapter类,通过继承需要被适配的类,实现我们想要的类接口,完成src->dst的适配。

我们现有的src类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 介绍:src类: 我们有的220V电压
* 作者:zhangxutong
* 邮箱:zhangxutong@imcoming.com
* 时间: 2016/10/18.
*/

public class Voltage220 {
public int output220V() {
int src = 220;
System.out.println("我是" + src + "V");
return src;
}
}

我们想要的dst接口:

1
2
3
4
5
6
7
8
9
10
/**
* 介绍:dst接口:客户需要的5V电压
* 作者:zhangxutong
* 邮箱:zhangxutong@imcoming.com
* 时间: 2016/10/18.
*/

public interface Voltage5 {
int output5V();
}

适配器类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 介绍:Adapter类:完成220V-5V的转变
* 通过继承src类,实现 dst 类接口,完成src->dst的适配。
* 作者:zhangxutong
* 邮箱:zhangxutong@imcoming.com
* 时间: 2016/10/18.
*/

public class VoltageAdapter extends Voltage220 implements Voltage5 {
@Override
public int output5V() {
int src = output220V();
System.out.println("适配器工作开始适配电压");
int dst = src / 44;
System.out.println("适配完成后输出电压:" + dst);
return dst;
}
}

对象的适配器模式(常用)

基本思路和类的适配器模式相同,只是将Adapter类作修改,这次不继承src类,而是持有src类的实例,以解决兼容性的问题。
即:持有 src类,实现 dst 类接口,完成src->dst的适配。
(根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系,因此大部分结构型模式都是对象结构型模式。)

Adapter类如下:

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
/**
* 介绍:对象适配器模式:
* 持有 src类,实现 dst 类接口,完成src->dst的适配。 。以达到解决**兼容性**的问题。
* 作者:zhangxutong
* 邮箱:zhangxutong@imcoming.com
* 时间: 2016/10/18.
*/

public class VoltageAdapter2 implements Voltage5 {
private Voltage220 mVoltage220;

public VoltageAdapter2(Voltage220 voltage220) {
mVoltage220 = voltage220;
}

@Override
public int output5V() {
int dst = 0;
if (null != mVoltage220) {
int src = mVoltage220.output220V();
System.out.println("对象适配器工作,开始适配电压");
dst = src / 44;
System.out.println("适配完成后输出电压:" + dst);
}
return dst;
}
}

对象适配器和类适配器其实算是同一种思想,只不过实现方式不同。
根据合成复用原则,组合大于继承,
所以它解决了类适配器必须继承src的局限性问题,也不再强求dst必须是接口。
同样的它使用成本更低,更灵活。

(和装饰者模式初学时可能会弄混,这里要搞清,装饰者是对src的装饰,使用者毫无察觉到src已经被装饰了(使用者用法不变)。 这里对象适配以后,使用者的用法还是变的。
即,装饰者用法: setSrc->setSrc,对象适配器用法:setSrc->setAdapter.)

接口的适配器模式

定义:

当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,它适用于一个接口不想使用其所有的方法的情况。

七、桥接模式(Bridge)

将抽象与实现分离开来,使得它们可以独立变化。

核心要点

  • 处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立的继承结构,使各个维度可以独立的扩展在抽象层建立关联。

效果及实现要点

  1. 桥接模式使用对象见的组合关系解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。
  2. 所谓抽象和实现沿着各自维度的变化,即“子类化”它们,得到各个子类之后,便可以任意它们,从而获得不同路上的不同其次。
  3. 桥接模式有时候类似于多继承方案,但是多继承方案往往违背了SRP原则,复用性较差。桥接模式是比继承方案更好的解决方法。
  4. 桥接模式的应用一般在“两个非常强的变化维度”,有时候即使有两个变化的维度,但是某个方向的变化维度并不剧烈——换而言之两个变化不会导致纵横交错的结果,并不一定要使用桥接模式。

使用场景

  1. 如果你不希望在抽象和实现部分采用固定的绑定关系,可以采用桥接模式,来把抽象和实现部分分开,然后在程序运行期间来动态的设置抽象部分需要用到的具体的实现,还可以动态切换具体的实现。
  2. 如果出现抽象部分和实现部分都应该可以扩展的情况,可以采用桥接模式,让抽象部分和实现部分可以独立的变化,从而可以灵活的进行单独扩展,而不是搅在一起,扩展一边会影响到另一边。
  3. 如果希望实现部分的修改,不会对客户产生影响,可以采用桥接模式,客户是面向抽象的接口在运行,实现部分的修改,可以独立于抽象部分,也就不会对客户产生影响了,也可以说对客户是透明的。
  4. 如果采用继承的实现方案,会导致产生很多子类,对于这种情况,可以考虑采用桥接模式,分析功能变化的原因,看看是否能分离成不同的纬度,然后通过桥接模式来分离它们,从而减少子类的数目。

实际开发中应用场景

  • JDBC驱动程序
    • AWT中的Peer架构
  • 银行日志管理:
    • 格式分类:操作日志、交易日志、异常日志
    • 距离分类:本地记录日志、异地记录日志
  • 人力资源系统中的奖金计算模块:
    • 奖金分类:个人奖金、团体奖金、激励奖金。
    • 部门分类:人事部门、销售部门、研发部门。
  • OA系统中的消息处理:
    • 业务类型:普通消息、加急消息、特急消息
    • 发送消息方式:系统内消息、手机短信、邮件

八、组合模式(composite)

将对象组合成树形结构来表示“整体/部分”层次关系,允许用户以相同的方式处理单独对象和组合对象。

组合模式核心:

  • 组合部件(Component):它是一个抽象角色,为要组合的对象提供统一的接口。
  • 叶子(Leaf):在组合中表示子节点对象,叶子节点不能有子节点。
  • 合成部件(Composite):定义有枝节点的行为,用来存储部件,实现在Component接口中的有关操作,如增加(Add)和删除(Remove)

组件(Component)类是组合类(Composite)和叶子类(Leaf)的父类,可以把组合类看成是树的中间节点。

组合对象拥有一个或者多个组件对象,因此组合对象的操作可以委托给组件对象去处理,而组件对象可以是另一个组合对象或者叶子对象。

透明模式

在Component中声明所有来管理子对象的方法,其中包括Add,Remove等。这样实现Component接口的所有子类都具备了Add和Remove方法。这样做的好处是叶节点和枝节点对于外界没有区别,它们具备完全一致的接口。

component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class Component {

protected String name;

public Component(String name) {
this.name = name;
}

//增加一个叶子构件或树枝构件
public abstract void add(Component component);

//删除一个叶子构件或树枝构件
public abstract void remove(Component component);

//获取分支下的所有叶子构件和树枝构件
public abstract void display(int depth);

}

composite:

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
public class Composite extends Component {

public Composite(String name) {
super(name);
}

//构建容器
private ArrayList<Component> componentArrayList = new ArrayList<Component>();

@Override
public void add(Component component) {
this.componentArrayList.add(component);
}

@Override
public void remove(Component component) {
this.componentArrayList.remove(component);
}

@Override
public void display(int depth) {
//输出树形结构
for(int i=0; i<depth; i++) {
System.out.print('-');
}
System.out.println(name);

//下级遍历
for (Component component : componentArrayList) {
component.display(depth + 1);
}
}

}

leaf:

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
public class Leaf extends Component {

public Leaf(String name) {
super(name);
}

@Override
public void add(Component component) {
//空实现,抛出“不支持请求”异常
throw new UnsupportedOperationException();
}

@Override
public void remove(Component component) {
//空实现,抛出“不支持请求”异常
throw new UnsupportedOperationException();
}

@Override
public void display(int depth) {
//输出树形结构的叶子节点
for(int i=0; i<depth; i++) {
System.out.print('-');
}
System.out.println(name);
}

}

弊端:客户端对叶节点和枝节点是一致的,但叶节点并不具备Add和Remove的功能,因而对它们的实现是没有意义的

安全模式

安全模式是把树枝节点和树叶节点彻底分开,树枝节点单独拥有用来组合的方法,这种方法比较安全。

component:

这里相比透明模式就少了add()和romove()抽象方法的声明。

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class Component {

protected String name;

public Component(String name) {
this.name = name;
}

//获取分支下的所有叶子构件和树枝构件
public abstract void display(int depth);

}

composite:

这里add()和remove()方法的实现就从继承变为了自己实现。

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
public class Composite extends Component {

public Composite(String name) {
super(name);
}

//构建容器
private ArrayList<Component> componentArrayList = new ArrayList<Component>();

//增加一个叶子构件或树枝构件
public void add(Component component) {
this.componentArrayList.add(component);
}

//删除一个叶子构件或树枝构件
public void remove(Component component) {
this.componentArrayList.remove(component);
}

@Override
public void display(int depth) {
//输出树形结构
for(int i=0; i<depth; i++) {
System.out.print('-');
}
System.out.println(name);

//下级遍历
for (Component component : componentArrayList) {
component.display(depth + 1);
}
}

}

leaf:

叶子节点中没有了空实现,比较安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Leaf extends Component {

public Leaf(String name) {
super(name);
}

@Override
public void display(int depth) {
//输出树形结构的叶子节点
for(int i=0; i<depth; i++) {
System.out.print('-');
}
System.out.println(name);
}

}

组合模式的应用

  1. 何时使用

  • 想表达“部分-整体”层次结构(树形结构)时
  • 希望用户忽略组合对象与单个对象的不同,用户将统一的使用组合结构中的所有对象

  2. 方法

  • 树枝和叶子实现统一接口,树枝内部组合该接口

  3. 优点

  • 高层模块调用简单。一棵树形机构中的所有节点都是Component,局部和整体对调用者来说没有任何区别,高层模块不必关心自己处理的是单个对象还是整个组合结构。
  • 节点自由增加

  4. 缺点

  • 使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒转原则

  5. 使用场景

  • 维护和展示部分-整体关系的场景(如树形菜单、文件和文件夹管理)
  • 从一个整体中能够独立出部分模块或功能的场景

  6. 应用实例

  • Swing中,Button、Checkbox等组件都是树叶,而Container容器是树枝
  • 文本编辑时,可以单个字编辑,也可以整段编辑,还可以全文编辑
  • 文件复制时,可以一个一个文件复制,也可以整个文件夹复制

参考文章

简说设计模式——组合模式 - JAdam - 博客园 (cnblogs.com)

三、建造者模式(Builder)

建造者模式可以将一个类的构建和表示进行分离

适用场景:

  • 隔离复杂对象的创建和使用,相同的方法,不同执行顺序,产生不同事件结果
  • 多个部件都可以装配到一个对象中,但产生的运行结果不相同
  • 产品类非常复杂或者产品类因为调用顺序不同而产生不同作用
  • 初始化一个对象时,参数过多,或者很多参数具有默认值
  • Builder模式不适合创建差异性很大的产品类
    产品内部变化复杂,会导致需要定义很多具体建造者类实现变化,增加项目中类的数量,增加系统的理解难度和运行成本
  • 需要生成的产品对象有复杂的内部结构,这些产品对象具备共性;

开发中应用场景:

  • StringBuilder类的append方法
  • SQL中的PreparedStatement
  • JDOM中,DomBuilder、SAXBuilder

参考文章:

一篇文章就彻底弄懂建造者模式(Builder Pattern) - 简书 (jianshu.com)

四、原型模式(prototype)

使用原型实例指定要创建对象的类型,通过复制这个原型来创建新对象。

适用场景

  • 通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式
  • 就是java中的克隆技术,以某个对象为原型,复制出新的对象。显然,新的对象具备原型对象的特点。优势有:效率高(直接克隆,避免了重新执行构造过程步骤)
  • 克隆类似于new,但是不同于new。new创建新的对象属性采用的是默认值。克隆出的 对象的属性值完全和原型对象相同。并且克隆出的新对象改变不会影响原型对象。然后, 再修改克隆对象的值。

原型模式实现

  • Cloneable接口和clone方法
  • Prototype模式中实现起来最困难的地方就是内存复制操作,所幸在Java中提供了 clone()方法替我们做了绝大部分事情。

例子:

1
2
3
public abstract class Prototype {
abstract Prototype myClone();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ConcretePrototype extends Prototype {

private String filed;

public ConcretePrototype(String filed) {
this.filed = filed;
}

@Override
Prototype myClone() {
return new ConcretePrototype(filed);
}

@Override
public String toString() {
return filed;
}
}
1
2
3
4
5
6
7
public class Client {
public static void main(String[] args) {
Prototype prototype = new ConcretePrototype("abc");
Prototype clone = prototype.myClone();
System.out.println(clone.toString());
}
}
1
abc

面向对象思想

类图

泛化关系(Generalization)

用来描述继承关系,在 Java 中使用 extends 关键字。

实现关系 (Realization)

用来实现一个接口,在 Java 中使用 implements 关键字。

关联关系 (Association)

表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。

【代码表现】:成员变量

聚合关系 (Aggregation)

表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。

聚合关系是关联关系的一种,是强的关联关系;关联和聚合在语法上无法区分,必须考察具体的逻辑关系。

【代码表现】:成员变量

组合关系 (Composition)

和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。

组合关系是关联关系的一种,是比聚合关系还要强的关系,它要求普通的聚合关系中代表整体的对象负责代表部分的对象的生命周期

【代码表现】:成员变量

依赖关系 (Dependency)

和关联关系不同的是,依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:

  • A 类是 B 类方法的局部变量;
  • A 类是 B 类方法的参数;
  • A 类向 B 类发送消息,从而影响 B 类发生变化。

【代码表现】:局部变量、方法的参数或者对静态方法的调用

各种关系的强弱顺序

泛化=实现>组合>聚合>关联>依赖

面向对象的六大原则

1、单一职责原则SRP

面向对象的六大原则就一个类而言,应该仅有一个引起它变化的原因。

简单的说就是:一个类中应该是一组相关性很高的函数、数据的封装。两个不一样的功能不应该放在一个类中。

这个原则没有具体的划分界限,需要根据个人经验,具体业务逻辑而定。这也是优化代码的第一步。试想一下,如果所有的功能写在一个类里,那么这个类会越来越大,越来越复杂,越不易修改维护。那么根据功能,各自独立拆分出来,岂不是逻辑会清晰些。

2、开闭原则OCP

定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的。当软件需要变化时,我们尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现(可能引入的代码会破坏原有系统)。

3、里氏替换原则LSP

定义:所有引用父类的地方,必须能使用子类的对象。简单地说就是将父类替换为他的子类是不会出现问题,反之,未必可以。

那么里氏替换原则就是依赖于面向对象语言的继承多态。核心原理是抽象

这里列举一下继承的优缺点:
优点:
(1)代码重用,减少创建类的成本,每个子类都拥有父类的方法与属性。
(2)子类与父类基本相似,但与父类又有所区别。
(3)提高代码的可扩展性。
缺点:
(1)继承是侵入性的,只要继承就必须拥有父类所有的属性与方法。
(2)可能造成子类代码冗余、灵活性降低。

开闭原则和里氏替换原则是生死相依的、不离不弃的。他们都强调了抽象这一重要的特性。

看着定义很是抽象,但是通俗的理解就是由子类实例化的父类引用,在使用这个引用时,感觉就像是使用了父类一样。一个简单的例子:

1
2
3
4
5
6
7
8
9
public class T{
private class A{...}
class B extends A{...}
public static void main(String[] args){
A a = new B();
// 类型是A,但实际是B类型实例化的,就是依赖多态
a.method();
}
}

4、依赖倒置原则DIP

定义:指代一种特定的解耦方式,使得高层次的模块不依赖于低层次的模块的实现细节的目的。他有一下几个关键点:
(1)高层模块不依赖于低层模块,应该都依赖其抽象。
(2)抽象不依赖细节。
(3)细节应依赖抽象。

解释:在Java中,抽象就是指接口或者抽象类,两者都是不能直接被实例化的细节就是实现类,实现接口或者继承抽象类而产生的就是细节,也就是可以加上一个关键字new产生的对象。高层模块就是调用端,底层模块就是具体实现类。

依赖倒置原则在Java中的表现就是:模块间通过抽象发生,实现类之间不发生直接依赖关系,其依赖关系是通过接口或者抽象类产生的。如果类与类直接依赖细节,那么就会直接耦合,那么当修改时,就会同时修改依赖者代码,这样限制了可扩展性。

5、接口隔离原则ISP

定义:类间的依赖关系应该建立在最小的接口上,将庞大、臃肿的接口拆分成更小的、更具体的接口。目的是系统的解耦,从而更容易重构、更改和重新部署。

6、迪米特原则LOD

定义:一个类应该对自己需要耦合或者调用的类知道的最少,类的内部如何实现与调用者或者依赖者没有关系,调用者或依赖者只需知道他需要的方法,其他可以一概不管。这样使得系统具有更低的耦合与更好的可扩展性。

类加载全过程

一个Java文件从编码完成到最终执行,一般主要包括两个过程

  • 编译

    即把java文件,通过javac命令编译成字节码,也就是.class文件。

  • 运行

    则是把编译生成的.class文件交给Java虚拟机(JVM)执行。

类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。

JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次

类加载

类加载的过程主要分为三个部分:

  • 加载
  • 链接
  • 初始化

而链接又可以细分为三个小部分:

  • 验证
  • 准备
  • 解析

img

加载

加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。

这里有两个重点:

  • 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译
  • 类加载器。一般包括启动类加载器扩展类加载器应用类加载器,以及用户的自定义类加载器

注:为什么会有自定义类加载器?

  • 一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。
  • 另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。

验证

主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。

包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?

对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?

对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。

对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?

准备

主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值

特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。

比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static tmp = 456, 那么该阶段tmp的初值就是456

解析

将常量池内的符号引用替换为直接引用的过程。

两个重点:

  • 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
  • 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量

举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。

在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

初始化

这个阶段主要是对类变量初始化,是执行类构造器的过程。类构造器()方法是由编译器自动收集类中的所有类变量的赋值动作和**静态语句块(static块)**中的语句合并产生的。

换句话说,只对static修饰的变量或语句进行初始化。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

  • 虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。

Java程序初始化顺序:

1、父类的静态变量
2、父类的静态代码块
3、子类的静态变量
4、子类的静态代码块
5、父类的非静态变量
6、父类的非静态代码块
7、父类的构造方法
8、子类的非静态变量
9、子类的非静态代码块
10、子类的构造方法

类加载时机

  1. 创建类的实例,也就是new一个对象
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(Class.forName(“com.lyj.load”))
  5. 初始化一个类的子类(会首先初始化子类的父类)
  6. JVM启动时标明的启动类,即文件名和类名相同的那个类

除此之外,下面几种情形需要特别指出:

对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

类的引用

主动引用(一定会初始化)

当虚拟机启动,先初始化main方法所在的类
1.new一个类的对象
2.调用类的静态成员(除了final常量)和静态方法
3.使用java.lang.reflect包的方法对类进行反射调用
4.当初始化一个类的时候,如果其父类没有被初始化,则会先初始化它的父类

被动引用

1.访问一个静态域时,只有真正的声名这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类被初始化
2.通过数组引用定义类时,不会触发此类的初始化
3.引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class Demo3 {
static{
System.out.println("mian方法所在类被初始化");
}
public static void main(String[] args) throws ClassNotFoundException{
/*
类的主动引用,一定会引起类的初始化 当虚拟机启动,先初始化main方法所在的类
1.new一个类的对象
2.调用类的静态成员(除了final常量)和静态方法
3.使用java.lang.reflect包的方法对类进行反射调用
4.当初始化一个类的时候,如果其父类没有被初始化,则会先初始化它的父类
*/
//new一个类的对象
//Animal animal = new Animal();//mian方法所在类被初始化 父类被初始化
//调用类的静态成员(除了final常量)和静态方法
//System.out.println(Animal.num);//mian方法所在类被初始化 3
//System.out.println(Animal.name);//mian方法所在类被初始化 父类被初始化 狗
//Animal.print();//mian方法所在类被初始化 父类被初始化 动物在叫
//使用java.lang.reflect包的方法对类进行反射调用
//Class c1 = Class.forName("exam.reflect.Cat");// mian方法所在类被初始化 父类被初始化 子类被初始化
//当初始化一个类的时候,如果其父类没有被初始化,则会先初始化它的父类
//Cat cat = new Cat();// mian方法所在类被初始化 父类被初始化 子类被初始化
/*
类的被动引用 不会发生类的初始化
1.访问一个静态域时,只有真正的声名这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类被初始化
2.通过数组引用定义类时,不会触发此类的初始化
3.引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)
*/
//子类引用父类的静态变量,不会子类被初始化
//System.out.println(Cat.name);mian方法所在类被初始化 父类被初始化 狗通过数组引用定义类时,不会触发此类的初始化
//Animal[] animals = new Animal[5];//mian方法所在类被初始化
//Cat[] cats = new Cat[6];//mian方法所在类被初始化
//引用常量不会触发此类的初始化
//System.out.println(Animal.NAME);//mian方法所在类被初始化 动物

}
}
class Animal{
static{
System.out.println("父类被初始化");
name = "猫";
}
static String name = "狗";
static int num = 3;
public static final String NAME = "动物";
public static void print(){
System.out.println("动物在叫");
}
} class Cat extends Animal{
static {
System.out.println("子类被初始化");
}
static String color = "黄色";
}

类加载器的原理

类缓存

标准的Java SE类加载器可以按要求查找类,一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过,JVM垃圾收集器可以回收这些Class对象。

类加载器分类

img

A.从Java虚拟机的角度:

1.Bootstrap ClassLoader启动类加载器
2.其他类加载器
从JVM的角度,加载器只分为两类,即JVM自身实现的Bootstrap启动类加载器,和其他JVM以外的所有类加载器。Bootstrap翻译为根,故也叫根类加载器。

B.从开发者的角度:

1.Bootstrap ClassLoader根类加载器
2.Extension ClassLoader拓展类加载器
3.Application ClassLoader应用程序类加载器
1.根类加载器,加载位于/jre/lib目录中的或者被参数-Xbootclasspath所指定的目录下的核心Java类库。此类加载器是Java虚拟机的一部分,使用native代码(C++)编写。

2.扩展类加载器,加载位于/jre/lib/ext目录中的或者java.ext.dirs系统变量所指定的目录下的拓展类库。此加载器由sun.misc.Launcher ExtClassLoader实现。

3.系统类加载器,加载用户路径(ClassPath)上所指定的类库。此加载器由sun.misc.Launcher$ AppClassLoader实现。

引导类加载器(bootstrap class loader)
(1)它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar,sun.boot.class.path路径下的内容),是用原生代码(C语言)来实现的,并不继承自 java.lang.ClassLoader。
(2)加载扩展类和应用程序类加载器。并指定他们的父类加载器。

扩展类加载器(extensions class loader)
(1)用来加载 Java 的扩展库(JAVA_HOME/jre/ext/*.jar,或java.ext.dirs路径下的内容) 。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java类。
(2)由sun.misc.Launcher$ExtClassLoader实现。

应用程序类加载器(application class loader)
(1)它根据 Java 应用的类路径(classpath,java.class.path 路径下的内容)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。
(2)由sun.misc.Launcher$AppClassLoader实现。

类加载器加载Class大致要经过如下8个步骤:

  1. 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
  2. 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
  3. 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
  4. 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
  5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
  6. 从文件中载入Class,成功后跳至第8步。
  7. 抛出ClassNotFountException异常。
  8. 返回对应的java.lang.Class对象。

类加载器的代理模式

代理模式即是将指定类的加载交给其他的类加载器。常用双亲委托机制。

JVM的类加载机制主要有如下3种。

  • 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
  • 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
  • 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

1、双亲委托机制

双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,如果都不能加载则报错——ClassNotFoundException。这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

值得注意是,双亲委托机制是代理模式的一种,但并不是所有的类加载器都采用双亲委托机制。在tomcat服务器类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。

自定义类加载器

(1)首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2。

(2)委派类加载请求给父类加载器,如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3。

(3)调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(…), loadClass(…)转抛异常,终止加载过程(注意:这里的异常种类不止一种)。

- 注意:被两个类加载器加载的同一个类,JVM认为是不相同的类。

线程上下文类加载器

通常当你需要动态加载资源的时候 , 你至少有三个 ClassLoader 可以选择 :
1.系统类加载器或叫作应用类加载器 (system classloader or application classloader)
2.当前类加载器
3.当前线程类加载器

• 当前线程类加载器是为了抛弃双亲委派加载链模式。
每个线程都有一个关联的上下文类加载器。如果你使用new Thread()方式生成新的线程,新线程将继承其父线程的上下文类加载器。如果程序对线程上下文类加载器没有任何改动的话,程序中所有的线程将都使用系统类加载器作为上下文类加载器。
• Thread.currentThread().getContextClassLoader()

tomcat服务器的类加载器

每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式(不同于前面说的双亲委托机制),所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。但也是为了保证安全,这样核心库就不在查询范围之内。