Dunwu Blog

大道至简,知易行难

效率提升方法论

在智力水平相当的前提下,常常会发现:有些人做事,事倍功半;有些人做事,事半功倍。

做任何事,如果有了清晰的思路,正确的指导方针,肯定是比毫无头绪要高效很多。所以,现实中,常常会看到这样一种现象,优秀的人,往往全面优秀,干什么都出彩;而平庸的人,做什么都出不了成绩。

大多数人不是天才,想要变得优秀,唯一的途径就是:按照正确的习惯(方式方法),坚持不懈的努力进步(自律)。

我们日复一日做的事情,决定了我们是怎样的人。因此所谓卓越,并非指行为,而是习惯

We are what we repeatedly do. Excellence, then, is not an act, but a habit.

——莎士比亚

做计划常用方法

名称 图示 方式 优点 缺点
时间法 时间顺序 清晰明确
操作性强
时间分配不合理或
突出情况容易打乱计划
清单法 重要程度 要事优先
事无遗漏
未完成容易形成压力;
容易造成形式主义,为打卡而完成
三段法 完成状态 态度明了
有条不紊
灵活性差,只能电子版或软件实现编辑,
纸质版事项会重复
OKR 法 目标分解 目标导向
高效成事
适合复杂事情或大项目的分解执行跟进
分类法 八个方面 事事周全
面面俱到
越是想顾周全,越难周全,
面面俱到,也会事事难完成
四象限法 img 轻重缓急 要事优先
忽略次要
被要事牵着走,忽略了
人生应该适度娱乐的重要性
甘特图法 日期进度 进度直观
易于理解
进度条只能反映时间进度,
无法反映事项具体完成情况的进度
PDCA 法 流程顺序 流程推进
循环解决
流程化容易形成思维惯性,
并且缺乏压力难以形成创造性

W2H

5W2H 分析法是一种思考问题的启发式思维方式。5W2H 分析法用五个以 W 开头的英语单词和两个以 H 开头的英语单词进行设问,得到关键性问题的答案,最后总结归纳出问题的目标、解决思路、处理方法等,这就叫做 5W2H 法。

5W2H 分析法又叫七问分析法,是二战中美国陆军兵器修理部首创。这种分析法广泛用于企业管理和技术活动,对于决策和执行性的活动措施也非常有帮助,也有助于弥补考虑问题的疏漏。

5W2H 分析法的意义在于:避免遇到一个问题后,不知从何入手。通过设问方式,由点成线,由线成面,把问题的关键点串联起来,整理出问题的解决思路。

5W2H

  • why - 为什么?为什么要这么做?理由何在?原因是什么?
  • what - 是什么?目的是什么?作什么工作?
  • where - 何处?在哪里做?从哪里入手?
  • when - 何时?什么时间完成?什么时机最适宜?
  • who - 谁?有谁来承担?谁来完成?谁负责?
  • how - 怎么做?如何提高效率?如何实施?方法怎么样?
  • how much - 多少?做到什么程度?数量如何?质量水平如何?费用产出如何?

四象限原则

四象限原则是一种时间管理方式

有首歌唱出了大多数职场人的心声:时间都去哪儿了?

事情、任务太多,时间太少,分身乏术。

时间管理四象限法则是美国的管理学家科维提出的一个时间管理的理论,按处理顺序划分为:紧急又重要、重要不紧急、紧急不重要、不紧急不重要。

img

  • 第一象限(重要而紧急

    • 案例:应付难缠的客户、准时完成工作、住院开刀等等。
    • 这是考验我们的经验、判断力的时刻,也是可以用心耕耘的园地。如果荒废了,我们很会可能变成行尸走肉。但我们也不能忘记,很多重要的事都是因为一拖再拖或事前准备不足,而变成迫在眉睫。
    • 该象限的本质是缺乏有效的工作计划导致本处于“重要但不紧急”第二象限的事情转变过来的,这也是传统思维状态下的管理者的通常状况,就是“忙”。
  • 第二象限(重要但不紧急)

    • 案例:学习新技能、建立人际关系、保持身体健康、长期的规划、问题的发掘与预防、参加培训、向上级提出问题处理的建议等等事项。
    • 荒废这个领域将使第一象限日益扩大,使我们陷入更大的压力,在危机中疲于应付。反之,多投入一些时间在这个领域有利于提高实践能力,缩小第一象限的范围。做好事先的规划、准备与预防措施,很多急事将无从产生。这个领域的事情不会对我们造成催促力量,所以必须主动去做,这是发挥个人领导力的领域。
    • 这更是传统低效管理者与高效卓越管理者的重要区别标志,建议管理者要把 80%的精力投入到该象限的工作,以使第一象限的“急”事无限变少,不再瞎“忙”。
  • 第三象限(紧急但不重要)

    • 案例:电话、会议、突发的访客都属于这一类。
    • 表面看似第一象限,因为迫切的呼声会让我们产生“这件事很重要”的错觉——实际上就算重要也是对别人而言。我们花很多时间在这个里面打转,自以为是在第一象限,其实不过是在满足别人的期望与标准。
  • 第四象限(不紧急也不重要)

    • 案例:阅读无聊小说、看毫无内容的电视节目、办公室聊天、刷微博、刷朋友圈等。
    • 简而言之就是浪费生命,所以根本不值得花半点时间在这个象限。但我们往往在一、三象限来回奔走,忙得焦头烂额,不得不到第四象限去疗养一番再出发。这部分范围倒不见得都是休闲活动,因为真正有创造意义的休闲活动是很有价值的。然而像阅读令人上瘾的无聊小说、毫无内容的电视节目、办公室聊天等。这样的休息不但不是为了走更长的路,反而是对身心的毁损,刚开始时也许有滋有味,到后来你就会发现其实是很空虚的。

HBase 快速入门

HBase 简介

为什么需要 HBase

在介绍 HBase 之前,我们不妨先了解一下为什么需要 HBase,或者说 HBase 是为了达到什么目的而产生。

在 HBase 诞生之前,Hadoop 可以通过 HDFS 来存储结构化、半结构甚至非结构化的数据,它是传统数据库的补充,是海量数据存储的最佳方法,它针对大文件的存储,批量访问和流式访问都做了优化,同时也通过多副本解决了容灾问题。

Hadoop 的缺陷在于:它只能执行批处理,并且只能以顺序方式访问数据。这意味着即使是最简单的工作,也必须搜索整个数据集,即:Hadoop 无法实现对数据的随机访问。实现数据的随机访问是传统的关系型数据库所擅长的,但它们却不能用于海量数据的存储。在这种情况下,必须有一种新的方案来同时解决海量数据存储和随机访问的问题,HBase 就是其中之一 (HBase,Cassandra,couchDB,Dynamo 和 MongoDB 都能存储海量数据并支持随机访问)。

注:数据结构分类:

  • 结构化数据:即以关系型数据库表形式管理的数据;
  • 半结构化数据:非关系模型的,有基本固定结构模式的数据,例如日志文件、XML 文档、JSON 文档、Email 等;
  • 非结构化数据:没有固定模式的数据,如 WORD、PDF、PPT、EXL,各种格式的图片、视频等。

什么是 HBase

HBase 是一个构建在 HDFS(Hadoop 文件系统)之上的列式数据库

HBase 是一种类似于 Google’s Big Table 的数据模型,它是 Hadoop 生态系统的一部分,它将数据存储在 HDFS 上,客户端可以通过 HBase 实现对 HDFS 上数据的随机访问。

img

HBase 的核心特性如下:

  • 分布式
    • 伸缩性:支持通过增减机器进行水平扩展,以提升整体容量和性能
    • 高可用:支持 RegionServers 之间的自动故障转移
    • 自动分区:Region 分散在集群中,当行数增长的时候,Region 也会自动的分区再均衡
  • 超大数据集:HBase 被设计用来读写超大规模的数据集(数十亿行至数百亿行的表)
  • 支持结构化、半结构化和非结构化的数据:由于 HBase 基于 HDFS 构建,所以和 HDFS 一样,支持结构化、半结构化和非结构化的数据
  • 非关系型数据库
    • 不支持标准 SQL 语法
    • 没有真正的索引
    • 不支持复杂的事务:只支持行级事务,即单行数据的读写都是原子性的

HBase 的其他特性

  • 读写操作遵循强一致性
  • 过滤器支持谓词下推
  • 易于使用的 Java 客户端 API
  • 它支持线性和模块化可扩展性。
  • HBase 表支持 Hadoop MapReduce 作业的便捷基类
  • 很容易使用 Java API 进行客户端访问
  • 为实时查询提供块缓存 BlockCache 和布隆过滤器
  • 它通过服务器端过滤器提供查询谓词下推

什么时候使用 HBase

根据上一节对于 HBase 特性的介绍,我们可以梳理出 HBase 适用、不适用的场景:

HBase 不适用场景:

  • 需要索引
  • 需要复杂的事务
  • 数据量较小(比如:数据量不足几百万行)

HBase 适用场景:

  • 能存储海量数据并支持随机访问(比如:数据量级达到十亿级至百亿级)
  • 存储结构化、半结构化数据
  • 硬件资源充足

一言以蔽之——HBase 适用的场景是:实时地随机访问超大数据集

HBase 的典型应用场景

  • 存储监控数据
  • 存储用户/车辆 GPS 信息
  • 存储用户行为数据
  • 存储各种日志数据,如:访问日志、操作日志、推送日志等。
  • 存储短信、邮件等消息类数据
  • 存储网页数据

HBase 数据模型简介

前面已经提及,HBase 是一个列式数据库,其数据模型和关系型数据库有所不同。其数据模型的关键术语如下:

  • Table:HBase 表由多行组成。
  • Row:HBase 中的一行由一个行键和一个或多个列以及与之关联的值组成。 行在存储时按行键的字母顺序排序。 为此,行键的设计非常重要。 目标是以相关行彼此靠近的方式存储数据。 常见的行键模式是网站域。 如果您的行键是域,您应该将它们反向存储(org.apache.www、org.apache.mail、org.apache.jira)。 这样,所有 Apache 域在表中彼此靠近,而不是根据子域的第一个字母展开。
  • Column:HBase 中的列由列族和列限定符组成,它们由 :(冒号)字符分隔。
  • Column Family:通常出于性能原因,列族在物理上将一组列及其值放在一起。 每个列族都有一组存储属性,例如它的值是否应该缓存在内存中,它的数据是如何压缩的,它的行键是如何编码的,等等。 表中的每一行都有相同的列族,尽管给定的行可能不在给定的列族中存储任何内容。
  • 列限定符:将列限定符添加到列族以提供给定数据片段的索引。 给定列族内容,列限定符可能是 content:html,另一个可能是 content:pdf。 尽管列族在表创建时是固定的,但列限定符是可变的,并且行之间可能有很大差异。
  • Cell:单元格是行、列族和列限定符的组合,包含一个值和一个时间戳,代表值的版本。
  • Timestamp:时间戳写在每个值旁边,是给定版本值的标识符。 默认情况下,时间戳表示写入数据时 RegionServer 上的时间,但您可以在将数据放入单元格时指定不同的时间戳值。

img

特性比较

HBase vs. RDBMS

RDBMS HBase
RDBMS 有它的模式,描述表的整体结构的约束 HBase 无模式,它不具有固定列模式的概念;仅定义列族
支持的文件系统有 FAT、NTFS 和 EXT 支持的文件系统只有 HDFS
使用提交日志来存储日志 使用预写日志 (WAL) 来存储日志
使用特定的协调系统来协调集群 使用 ZooKeeper 来协调集群
存储的都是中小规模的数据表 存储的是超大规模的数据表,并且适合存储宽表
通常支持复杂的事务 仅支持行级事务
适用于结构化数据 适用于半结构化、结构化数据
使用主键 使用 row key

HBase vs. HDFS

HDFS HBase
HDFS 提供了一个用于分布式存储的文件系统。 HBase 提供面向表格列的数据存储。
HDFS 为大文件提供优化存储。 HBase 为表格数据提供了优化。
HDFS 使用块文件。 HBase 使用键值对数据。
HDFS 数据模型不灵活。 HBase 提供了一个灵活的数据模型。
HDFS 使用文件系统和处理框架。 HBase 使用带有内置 Hadoop MapReduce 支持的表格存储。
HDFS 主要针对一次写入多次读取进行了优化。 HBase 针对读/写许多进行了优化。

行式数据库 vs. 列式数据库

行式数据库 列式数据库
对于添加/修改操作更高效 对于读取操作更高效
读取整行数据 仅读取必要的列数据
最适合在线事务处理系统(OLTP) 不适合在线事务处理系统(OLTP)
将行数据存储在连续的页内存中 将列数据存储在非连续的页内存中

列式数据库的优点:

  • 支持数据压缩
  • 支持快速数据检索
  • 简化了管理和配置
  • 有利于聚合查询(例如 COUNT、SUM、AVG、MIN 和 MAX)的高性能
  • 分区效率很高,因为它提供了自动分片机制的功能,可以将较大的区域分配给较小的区域

列式数据库的缺点:

  • JOIN 查询和来自多个表的数据未优化
  • 必须为频繁的删除和更新创建拆分,因此降低了存储效率
  • 由于非关系数据库的特性,分区和索引的设计非常困难

HBase 安装

HBase Hello World 示例

  1. 连接 HBase

    在 HBase 安装目录的 /bin 目录下执行 hbase shell 命令进入 HBase 控制台。

    1
    2
    $ ./bin/hbase shell
    hbase(main):001:0>
  2. 输入 help 可以查看 HBase Shell 命令。

  3. 创建表

    可以使用 create 命令创建一张新表。必须要指定表名和 Column Family。

    1
    2
    3
    4
    hbase(main):001:0> create 'test', 'cf'
    0 row(s) in 0.4170 seconds

    => Hbase::Table - test
  4. 列出表信息

    使用 list 命令来确认新建的表已存在。

    1
    2
    3
    4
    5
    6
    hbase(main):002:0> list 'test'
    TABLE
    test
    1 row(s) in 0.0180 seconds

    => ["test"]

    可以使用 describe 命令可以查看表的细节信息,包括配置信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    hbase(main):003:0> describe 'test'
    Table test is ENABLED
    test
    COLUMN FAMILIES DESCRIPTION
    {NAME => 'cf', VERSIONS => '1', EVICT_BLOCKS_ON_CLOSE => 'false', NEW_VERSION_BEHAVIOR => 'false', KEEP_DELETED_CELLS => 'FALSE', CACHE_DATA_ON_WRITE =>
    'false', DATA_BLOCK_ENCODING => 'NONE', TTL => 'FOREVER', MIN_VERSIONS => '0', REPLICATION_SCOPE => '0', BLOOMFILTER => 'ROW', CACHE_INDEX_ON_WRITE => 'f
    alse', IN_MEMORY => 'false', CACHE_BLOOMS_ON_WRITE => 'false', PREFETCH_BLOCKS_ON_OPEN => 'false', COMPRESSION => 'NONE', BLOCKCACHE => 'true', BLOCKSIZE
    => '65536'}
    1 row(s)
    Took 0.9998 seconds
  5. 向表中写数据

    可以使用 put 命令向 HBase 表中写数据。

    1
    2
    3
    4
    5
    6
    7
    8
    hbase(main):003:0> put 'test', 'row1', 'cf:a', 'value1'
    0 row(s) in 0.0850 seconds

    hbase(main):004:0> put 'test', 'row2', 'cf:b', 'value2'
    0 row(s) in 0.0110 seconds

    hbase(main):005:0> put 'test', 'row3', 'cf:c', 'value3'
    0 row(s) in 0.0100 seconds
  6. 一次性扫描表的所有数据

    使用 scan 命令来扫描表数据。

    1
    2
    3
    4
    5
    6
    hbase(main):006:0> scan 'test'
    ROW COLUMN+CELL
    row1 column=cf:a, timestamp=1421762485768, value=value1
    row2 column=cf:b, timestamp=1421762491785, value=value2
    row3 column=cf:c, timestamp=1421762496210, value=value3
    3 row(s) in 0.0230 seconds
  7. 查看一行数据

    使用 get 命令可以查看一行表数据。

    1
    2
    3
    4
    hbase(main):007:0> get 'test', 'row1'
    COLUMN CELL
    cf:a timestamp=1421762485768, value=value1
    1 row(s) in 0.0350 seconds
  8. 禁用表

    如果想要删除表或修改表设置,必须先使用 disable 命令禁用表。如果想再次启用表,可以使用 enable 命令。

    1
    2
    3
    4
    5
    hbase(main):008:0> disable 'test'
    0 row(s) in 1.1820 seconds

    hbase(main):009:0> enable 'test'
    0 row(s) in 0.1770 seconds
  9. 删除表

    使用 drop 命令可以删除表。

    1
    2
    hbase(main):011:0> drop 'test'
    0 row(s) in 0.1370 seconds
  10. 退出 HBase Shell

    使用 quit 命令,就能退出 HBase Shell 控制台。

参考资料

Nosql 技术选型

img

一、Nosql 简介

传统的关系型数据库存在以下缺点:

  • 大数据场景下 I/O 较高 - 因为数据是按行存储,即使只针对其中某一列进行运算,关系型数据库也会将整行数据从存储设备中读入内存,导致 I/O 较高。
  • 存储的是行记录,无法存储数据结构
  • 表结构 schema 扩展不方便 - 如要需要修改表结构,需要执行执行 DDL(data definition language),语句修改,修改期间会导致锁表,部分服务不可用。
  • 全文搜索功能较弱 - 关系型数据库下只能够进行子字符串的匹配查询,当表的数据逐渐变大的时候,LIKE 查询的匹配会非常慢,即使在有索引的情况下。况且关系型数据库也不应该对文本字段进行索引。
  • 存储和处理复杂关系型数据功能较弱 - 许多应用程序需要了解和导航高度连接数据之间的关系,才能启用社交应用程序、推荐引擎、欺诈检测、知识图谱、生命科学和 IT/网络等用例。然而传统的关系数据库并不善于处理数据点之间的关系。它们的表格数据模型和严格的模式使它们很难添加新的或不同种类的关联信息。

随着大数据时代的到来,越来越多的网站、应用系统需要支撑海量数据存储,高并发请求、高可用、高可扩展性等特性要求。传统的关系型数据库在应付这些调整已经显得力不从心,暴露了许多能以克服的问题。由此,各种各样的 NoSQL(Not Only SQL)数据库作为传统关系型数据的一个有力补充得到迅猛发展。

nosql-history

NoSQL,泛指非关系型的数据库,可以理解为 SQL 的一个有力补充。

在 NoSQL 许多方面性能大大优于非关系型数据库的同时,往往也伴随一些特性的缺失,比较常见的,是事务库事务功能的缺失。 数据库事务正确执行的四个基本要素:ACID 如下:

名称 描述
A Atomicity (原子性) 一个事务中的所有操作,要么全部完成,要么全部不完成,不会在中间某个环节结束。 事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
C Consistency 一致性 在事务开始之前和事务结束以后,数据的数据的一致性约束没有被破坏。
I Isolation 隔离性 数据库允许多个并发事务同时对数据进行读写和修改的能力。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
D Durability 持久性 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

下面介绍 5 大类 NoSQL 数据针对传统关系型数据库的缺点提供的解决方案:

二、列式数据库

列式数据库是以列相关存储架构进行数据存储的数据库,主要适合于批量数据处理和即时查询

相对应的是行式数据库,数据以行相关的存储体系架构进行空间分配,主要适合于小批量的数据处理,常用于联机事务型数据处理。

基于列式数据库的列列存储特性,可以解决某些特定场景下关系型数据库 I/O 较高的问题

列式数据库原理

传统关系型数据库是按照行来存储数据库,称为“行式数据库”,而列式数据库是按照列来存储数据。

将表放入存储系统中有两种方法,而我们绝大部分是采用行存储的。 行存储法是将各行放入连续的物理位置,这很像传统的记录和文件系统。 列存储法是将数据按照列存储到数据库中,与行存储类似,下图是两种存储方法的图形化解释:

按行存储和按列存储模式

列式数据库产品

  • HBase

    HBase

    HBase 是一个开源的非关系型分布式数据库(NoSQL),它参考了谷歌的 BigTable 建模,实现的编程语言为 Java。它是 Apache 软件基金会的 Hadoop 项目的一部分,运行于 HDFS 文件系统之上,为 Hadoop 提供类似于 BigTable 规模的服务。因此,它可以容错地存储海量稀疏的数据。

  • BigTable

    img

    BigTable 是一种压缩的、高性能的、高可扩展性的,基于 Google 文件系统(Google File System,GFS)的数据存储系统,用于存储大规模结构化数据,适用于云端计算。

列式数据库特性

优点如下:

  • 高效的储存空间利用率

列式数据库由于其针对不同列的数据特征而发明的不同算法,使其往往有比行式数据库高的多的压缩率,普通的行式数据库一般压缩率在 3:1 到 5:1 左右,而列式数据库的压缩率一般在 8:1 到 30:1 左右。 比较常见的,通过字典表压缩数据: 下面中才是那张表本来的样子。经过字典表进行数据压缩后,表中的字符串才都变成数字了。正因为每个字符串在字典表里只出现一次了,所以达到了压缩的目的(有点像规范化和非规范化 Normalize 和 Denomalize)

通过字典表压缩数据

  • 查询效率高

读取多条数据的同一列效率高,因为这些列都是存储在一起的,一次磁盘操作可以数据的指定列全部读取到内存中。 下图通过一条查询的执行过程说明列式存储(以及数据压缩)的优点

img

1
2
3
4
5
6
执行步骤如下:
i. 去字典表里找到字符串对应数字(只进行一次字符串比较)。
ii. 用数字去列表里匹配,匹配上的位置设为1
iii. 把不同列的匹配结果进行位运算得到符合所有条件的记录下标。
iv. 使用这个下标组装出最终的结果集。
复制代码
  • 适合做聚合操作
  • 适合大量的数据而不是小数据

缺点如下:

  • 不适合扫描小量数据
  • 不适合随机的更新
  • 不适合做含有删除和更新的实时操作
  • 单行的数据是 ACID 的,多行的事务时,不支持事务的正常回滚,支持 I(Isolation)隔离性(事务串行提交),D(Durability)持久性,不能保证 A(Atomicity)原子性, C(Consistency)一致性

列式数据库使用场景

以 HBase 为例说明:

  • 大数据量 (100s TB 级数据) 且有快速随机访问的需求。增长量无法预估的应用,需要进行优雅的数据扩展的 HBase 支持在线扩展,即使在一段时间内数据量呈井喷式增长,也可以通过 HBase 横向扩展来满足功能。
  • 写密集型应用,每天写入量巨大,而相对读数量较小的应用 比如 IM 的历史消息,游戏的日志等等
  • 不需要复杂查询条件来查询数据的应用 HBase 只支持基于 rowkey 的查询,对于 HBase 来说,单条记录或者小范围的查询是可以接受的,大范围的查询由于分布式的原因,可能在性能上有点影响,HBase 不适用于有 join,多级索引,表关系复杂的数据模型。
  • 对性能和可靠性要求非常高的应用,由于 HBase 本身没有单点故障,可用性非常高。
  • 存储结构化和半结构化的数据

三、K-V 数据库

K-V 数据库指的是使用键值(key-value)存储的数据库,其数据按照键值对的形式进行组织、索引和存储

KV 存储非常适合存储不涉及过多数据关系业务关系的数据,同时能有效减少读写磁盘的次数,比 SQL 数据库存储拥有更好的读写性能,能够解决关系型数据库无法存储数据结构的问题

K-V 数据库产品

  • Redis

    img

    Redis 是一个使用 ANSI C 编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。从 2015 年 6 月开始,Redis 的开发由 Redis Labs 赞助,而 2013 年 5 月至 2015 年 6 月期间,其开发由 Pivotal 赞助。在 2013 年 5 月之前,其开发由 VMware 赞助。根据月度排行网站 DB-Engines.com 的数据显示,Redis 是最流行的键值对存储数据库。

  • Cassandra

    img

    Apache Cassandra(社区内一般简称为 C*)是一套开源分布式 NoSQL 数据库系统。它最初由 Facebook 开发,用于储存收件箱等简单格式数据,集 Google BigTable 的数据模型与 Amazon Dynamo 的完全分布式架构于一身。Facebook 于 2008 将 Cassandra 开源,此后,由于 Cassandra 良好的可扩展性和性能,被 Apple, Comcast,Instagram, Spotify, eBay, Rackspace, Netflix 等知名网站所采用,成为了一种流行的分布式结构化数据存储方案。

  • LevelDB

    img

    LevelDB 是一个由 Google 公司所研发的键/值对(Key/Value Pair)嵌入式数据库管理系统编程库, 以开源的 BSD 许可证发布。

K-V 数据库特性

以 Redis 为例:

优点如下:

  • 性能极高 - Redis 能支持超过 10W 的 TPS。
  • 丰富的数据类型 - Redis 支持包括 String,Hash,List,Set,Sorted Set,Bitmap 和 hyperloglog。
  • 丰富的特性 - Redis 还支持 publish/subscribe、通知、key 过期等等特性。

缺点如下: 针对 ACID,Redis 事务不能支持原子性和持久性(A 和 D),只支持隔离性和一致性(I 和 C) 特别说明一下,这里所说的无法保证原子性,是针对 Redis 的事务操作,因为事务是不支持回滚(roll back),而因为 Redis 的单线程模型,Redis 的普通操作是原子性的

大部分业务不需要严格遵循 ACID 原则,例如游戏实时排行榜,粉丝关注等场景,即使部分数据持久化失败,其实业务影响也非常小。因此在设计方案时,需要根据业务特征和要求来做选择

K-V 数据库使用场景

  • 适用场景 - 储存用户信息(比如会话)、配置文件、参数、购物车等等。这些信息一般都和 ID(键)挂钩。

  • 不适用场景

    • 需要通过值来查询,而不是键来查询。Key-Value 数据库中根本没有通过值查询的途径。
    • 需要储存数据之间的关系。在 Key-Value 数据库中不能通过两个或以上的键来关联数据
    • 需要事务的支持。在 Key-Value 数据库中故障产生时不可以进行回滚。

四、文档数据库

文档数据库(也称为文档型数据库)是旨在将半结构化数据存储为文档的一种数据库,它可以解决关系型数据库表结构 schema 扩展不方便的问题。文档数据库通常以 JSON 或 XML 格式存储数据

由于文档数据库的 no-schema 特性,可以存储和读取任意数据。由于使用的数据格式是 JSON 或者 XML,无需在使用前定义字段,读取一个 JSON 中不存在的字段也不会导致 SQL 那样的语法错误。

文档数据库产品

  • MongoDB

    img

    MongoDB是一种面向文档的数据库管理系统,由 C++ 撰写而成,以此来解决应用程序开发社区中的大量现实问题。2007 年 10 月,MongoDB 由 10gen 团队所发展。2009 年 2 月首度推出。

  • CouchDB

    img

    Apache CouchDB 是一个开源数据库,专注于易用性和成为”完全拥抱 web 的数据库“。它是一个使用 JSON 作为存储格式,JavaScript 作为查询语言,MapReduce 和 HTTP 作为 API 的 NoSQL 数据库。其中一个显著的功能就是多主复制。CouchDB 的第一个版本发布在 2005 年,在 2008 年成为了 Apache 的项目。

文档数据库特性

以 MongoDB 为例进行说明

优点如下:

  • 容易存储复杂数据结构 - JSON 是一种强大的描述语言,能够描述复杂的数据结构。
  • 容易变更数据结构 - 无需像关系型数据库一样先执行 DDL 语句修改表结构,程序代码直接读写即可。
  • 容易兼容历史数据 - 对于历史数据,即使没有新增的字段,也不会导致错误,只会返回空值,此时代码兼容处理即可。

缺点如下:

  • 部分支持事务
    • Atomicity(原子性) 仅支持单行/文档级原子性,不支持多行、多文档、多语句原子性。
    • Isolation(隔离性) 隔离级别仅支持已提交读(Read committed)级别,可能导致不可重复读,幻读的问题。
  • 不支持复杂查询 - 例如 join 查询,如果需要 join 查询,需要多次操作数据库。

MongonDB 还是支持多文档事务的 Consistency(一致性)和 Durability(持久性)

虽然官方宣布 MongoDB 将在 4.0 版本中正式推出多文档 ACID 事务支持,最后落地情况还有待见证。

文档数据库使用场景

适用场景

  • 大数据量,且未来数据增长很快
  • 表结构不明确,且字段在不断增加,例如内容管理系统,信息管理系统

不适用场景

  • 支持事务 - 在不同的文档上需要添加事务。Document-Oriented 数据库并不支持文档间的事务
  • 支持复杂查询 - 多个文档直接需要复杂查询,例如 join

五、全文搜索引擎

传统关系型数据库主要通过索引来达到快速查询的目的,在全文搜索的业务下,索引也无能为力,主要体现在:

  • 全文搜索的条件可以随意排列组合,如果通过索引来满足,则索引的数量非常多
  • 全文搜索的模糊匹配方式,索引无法满足,只能用 LIKE 查询,而 LIKE 查询是整表扫描,效率非常低

而全文搜索引擎的出现,正是解决关系型数据库全文搜索功能较弱的问题

搜索引擎原理

全文搜索引擎的技术原理称为 **倒排索引(inverted index)**,是一种索引方法,其基本原理是建立单词到文档的索引。与之相对是,是“正排索引”,其基本原理是建立文档到单词的索引。

现在有如下文档集合:

img

正排索引得到索引如下:

img

可见,正排索引适用于根据文档名称查询文档内容

简单的倒排索引如下:

img

带有单词频率信息的倒排索引如下:

img

可见,倒排索引适用于根据关键词来查询文档内容

搜索引擎产品

  • Elasticsearch

    img

    Elasticsearch 是一个基于 Lucene 的搜索引擎。它提供了一个分布式,多租户 -能够全文搜索与发动机 HTTP Web 界面和无架构 JSON 文件。Elasticsearch 是用 Java 开发的,并根据 Apache License 的条款作为开源发布。根据 DB-Engines 排名,Elasticsearch 是最受欢迎的企业搜索引擎,后面是基于 Lucene 的 Apache Solr。

  • Solr

    img

    Solr 是 Apache Lucene 项目的开源企业搜索平台。其主要功能包括全文检索、命中标示、分面搜索、动态聚类、数据库集成,以及富文本(如 Word、PDF)的处理。Solr 是高度可扩展的,并提供了分布式搜索和索引复制

搜索引擎特性

以 Elasticsearch 为例: 优点如下:

  • 查询效率高 - 对海量数据进行近实时的处理
  • 可扩展性 - 基于集群环境可以方便横向扩展,可以承载 PB 级数据
  • 高可用 - Elasticsearch 集群弹性-他们将发现新的或失败的节点,重组和重新平衡数据,确保数据是安全的和可访问的

缺点如下:

  • 部分支持事务 - 单一文档的数据是 ACID 的,包含多个文档的事务时不支持事务的正常回滚,支持 I(Isolation)隔离性(基于乐观锁机制的),D(Durability)持久性,不支持 A(Atomicity)原子性,C(Consistency)一致性
  • 对类似数据库中通过外键的复杂的多表关联操作支持较弱。
  • 读写有一定延时,写入的数据,最快 1s 中能被检索到
  • 更新性能较低,底层实现是先删数据,再插入新数据
  • 内存占用大,因为 Lucene 将索引部分加载到内存中

搜索引擎场景

适用场景如下:

  • 搜索引擎和数据分析引擎 - 全文检索,结构化检索,数据分析
  • 对海量数据进行近实时的处理 - 可以将海量数据分散到多台服务器上去存储和检索

不适用场景如下:

  • 数据需要频繁更新
  • 需要复杂关联查询

六、图数据库

img

图形数据库应用图论存储实体之间的关系信息。最常见例子就是社会网络中人与人之间的关系。关系型数据库用于存储“关系型”数据的效果并不好,其查询复杂、缓慢、超出预期,而图形数据库的独特设计恰恰弥补了这个缺陷,解决关系型数据库存储和处理复杂关系型数据功能较弱的问题。

图数据库产品

  • Neo4j

    img

    Neo4j 是由 Neo4j,Inc。开发的图形数据库管理系统。由其开发人员描述为具有原生图存储和处理的符合 ACID 的事务数据库,根据 DB-Engines 排名, Neo4j 是最流行的图形数据库。

  • ArangoDB

    img

    ArangoDB 是由 triAGENS GmbH 开发的原生多模型数据库系统。数据库系统支持三个重要的数据模型(键/值,文档,图形),其中包含一个数据库核心和统一查询语言 AQL(ArangoDB 查询语言)。查询语言是声明性的,允许在单个查询中组合不同的数据访问模式。ArangoDB 是一个 NoSQL 数据库系统,但 AQL 在很多方面与 SQL 类似。

  • Titan

    img

    Titan 是一个可扩展的图形数据库,针对存储和查询包含分布在多机群集中的数百亿个顶点和边缘的图形进行了优化。Titan 是一个事务性数据库,可以支持数千个并发用户实时执行复杂的图形遍历。

图数据库特性

以 Neo4j 为例:

Neo4j 使用数据结构中图(graph)的概念来进行建模。 Neo4j 中两个最基本的概念是节点和边。节点表示实体,边则表示实体之间的关系。节点和边都可以有自己的属性。不同实体通过各种不同的关系关联起来,形成复杂的对象图。

针对关系数据,2 种 2 数据库的存储结构不同:

2种存储结构

Neo4j 中,存储节点时使用了”index-free adjacency”,即每个节点都有指向其邻居节点的指针,可以让我们在 O(1)的时间内找到邻居节点。另外,按照官方的说法,在 Neo4j 中边是最重要的,是”first-class entities”,所以单独存储,这有利于在图遍历的时候提高速度,也可以很方便地以任何方向进行遍历

img

如下优点:

  • 高性能 - 图的遍历是图数据结构所具有的独特算法,即从一个节点开始,根据其连接的关系,可以快速和方便地找出它的邻近节点。这种查找数据的方法并不受数据量的大小所影响,因为邻近查询始终查找的是有限的局部数据,不会对整个数据库进行搜索
  • 设计的灵活性 - 数据结构的自然伸展特性及其非结构化的数据格式,让图数据库设计可以具有很大的伸缩性和灵活性。因为随着需求的变化而增加的节点、关系及其属性并不会影响到原来数据的正常使用
  • 开发的敏捷性 - 直观明了的数据模型,从需求的讨论开始,到程序开发和实现,以及最终保存在数据库中的样子,它的模样似乎没有什么变化,甚至可以说本来就是一模一样的
  • 完全支持 ACID - 不像别的 NoSQL 数据库 Neo4j 还具有完全事务管理特性,完全支持 ACID 事务管理

缺点如下:

  • 存在支持节点,关系和属性的数量的限制。
  • 不支持拆分。

图数据库场景

适用场景如下:

  • 关系性强的数据中,如社交网络
  • 推荐引擎。如果我们将数据以图的形式表现,那么将会非常有益于推荐的制定

不适用场景如下:

  • 记录大量基于事件的数据(例如日志条目或传感器数据)
  • 对大规模分布式数据进行处理
  • 保存在关系型数据库中的结构化数据
  • 二进制数据存储

七、总结

关系型数据库和 NoSQL 数据库的选型,往往需要考虑几个指标:

  • 数据量
  • 并发量
  • 实时性
  • 一致性要求
  • 读写分布和类型
  • 安全性
  • 运维成本

常见软件系统数据库选型参考如下:

  • 中后台管理型系统 - 如运营系统,数据量少,并发量小,首选关系型数据库。
  • 大流量系统 - 如电商单品页,后台考虑选关系型数据库,前台考虑选内存型数据库。
  • 日志型系统 - 原始数据考虑选列式数据库,日志搜索考虑选搜索引擎。
  • 搜索型系统 - 例如站内搜索,非通用搜索,如商品搜索,后台考虑选关系型数据库,前台考虑选搜索引擎。
  • 事务型系统 - 如库存,交易,记账,考虑选关系型数据库+K-V 数据库(作为缓存)+分布式事务。
  • 离线计算 - 如大量数据分析,考虑选列式数据库或关系型数据。
  • 实时计算 - 如实时监控,可以考虑选内存型数据库或者列式数据库。

设计实践中,要基于需求、业务驱动架构,无论选用 RDB/NoSQL/DRDB,一定是以需求为导向,最终数据存储方案必然是各种权衡的综合性设计

参考资料

JavaWeb 之 Jsp 指南

简介

什么是 Java Server Pages

JSP全称Java Server Pages,是一种动态网页开发技术。

它使用 JSP 标签在 HTML 网页中插入 Java 代码。标签通常以 <% 开头以 %> 结束。

JSP 是一种 Java servlet,主要用于实现 Java web 应用程序的用户界面部分。网页开发者们通过结合 HTML 代码、XHTML 代码、XML 元素以及嵌入 JSP 操作和命令来编写 JSP。

JSP 通过网页表单获取用户输入数据、访问数据库及其他数据源,然后动态地创建网页。

JSP 标签有多种功能,比如访问数据库、记录用户选择信息、访问 JavaBeans 组件等,还可以在不同的网页中传递控制信息和共享信息。

为什么使用 JSP

JSP 也是一种 Servlet,因此 JSP 能够完成 Servlet 能完成的任何工作。

JSP 程序与 CGI 程序有着相似的功能,但和 CGI 程序相比,JSP 程序有如下优势:

  • 性能更加优越,因为 JSP 可以直接在 HTML 网页中动态嵌入元素而不需要单独引用 CGI 文件。
  • 服务器调用的是已经编译好的 JSP 文件,而不像 CGI/Perl 那样必须先载入解释器和目标脚本。
  • JSP 基于 Java Servlets API,因此,JSP 拥有各种强大的企业级 Java API,包括 JDBC,JNDI,EJB,JAXP 等等。
  • JSP 页面可以与处理业务逻辑的 servlets 一起使用,这种模式被 Java servlet 模板引擎所支持。

最后,JSP 是 Java EE 不可或缺的一部分,是一个完整的企业级应用平台。这意味着 JSP 可以用最简单的方式来实现最复杂的应用。

JSP 的优势

以下列出了使用 JSP 带来的其他好处:

  • 与 ASP 相比:JSP 有两大优势。首先,动态部分用 Java 编写,而不是 VB 或其他 MS 专用语言,所以更加强大与易用。第二点就是 JSP 易于移植到非 MS 平台上。
  • 与纯 Servlets 相比:JSP 可以很方便的编写或者修改 HTML 网页而不用去面对大量的 println 语句。
  • 与 SSI 相比:SSI 无法使用表单数据、无法进行数据库链接。
  • 与 JavaScript 相比:虽然 JavaScript 可以在客户端动态生成 HTML,但是很难与服务器交互,因此不能提供复杂的服务,比如访问数据库和图像处理等等。
  • 与静态 HTML 相比:静态 HTML 不包含动态信息。

JSP 工作原理

JSP 是一种 Servlet,但工作方式和 Servlet 有所差别。

Servlet 是先将源代码编译为 class 文件后部署到服务器下的,先编译后部署

Jsp 是先将源代码部署到服务器再编译,先部署后编译

Jsp 会在客户端第一次请求 Jsp 文件时被编译为 HttpJspPage 类(Servlet 的一个子类)。该类会被服务器临时存放在服务器工作目录里。所以,第一次请求 Jsp 后,访问速度会变快就是这个道理。

JSP 工作流程

网络服务器需要一个 JSP 引擎,也就是一个容器来处理 JSP 页面。容器负责截获对 JSP 页面的请求。本教程使用内嵌 JSP 容器的 Apache 来支持 JSP 开发。

JSP 容器与 Web 服务器协同合作,为 JSP 的正常运行提供必要的运行环境和其他服务,并且能够正确识别专属于 JSP 网页的特殊元素。

下图显示了 JSP 容器和 JSP 文件在 Web 应用中所处的位置。

img

工作步骤

以下步骤表明了 Web 服务器是如何使用 JSP 来创建网页的:

  • 就像其他普通的网页一样,您的浏览器发送一个 HTTP 请求给服务器。
  • Web 服务器识别出这是一个对 JSP 网页的请求,并且将该请求传递给 JSP 引擎。通过使用 URL 或者.jsp 文件来完成。
  • JSP 引擎从磁盘中载入 JSP 文件,然后将它们转化为 servlet。这种转化只是简单地将所有模板文本改用 println()语句,并且将所有的 JSP 元素转化成 Java 代码。
  • JSP 引擎将 servlet 编译成可执行类,并且将原始请求传递给 servlet 引擎。
  • Web 服务器的某组件将会调用 servlet 引擎,然后载入并执行 servlet 类。在执行过程中,servlet 产生 HTML 格式的输出并将其内嵌于 HTTP response 中上交给 Web 服务器。
  • Web 服务器以静态 HTML 网页的形式将 HTTP response 返回到您的浏览器中。
  • 最终,Web 浏览器处理 HTTP response 中动态产生的 HTML 网页,就好像在处理静态网页一样。

以上提及到的步骤可以用下图来表示:

一般情况下,JSP 引擎会检查 JSP 文件对应的 servlet 是否已经存在,并且检查 JSP 文件的修改日期是否早于 servlet。如果 JSP 文件的修改日期早于对应的 servlet,那么容器就可以确定 JSP 文件没有被修改过并且 servlet 有效。这使得整个流程与其他脚本语言(比如 PHP)相比要高效快捷一些。

JSP 生命周期

理解 JSP 底层功能的关键就是去理解它们所遵守的生命周期。

JSP 生命周期就是从创建到销毁的整个过程,类似于 servlet 生命周期,区别在于 JSP 生命周期还包括将 JSP 文件编译成 servlet。

以下是 JSP 生命周期中所走过的几个阶段:

  • 编译阶段:servlet 容器编译 servlet 源文件,生成 servlet 类
  • 初始化阶段:加载与 JSP 对应的 servlet 类,创建其实例,并调用它的初始化方法
  • 执行阶段:调用与 JSP 对应的 servlet 实例的服务方法
  • 销毁阶段:调用与 JSP 对应的 servlet 实例的销毁方法,然后销毁 servlet 实例

很明显,JSP 生命周期的四个主要阶段和 servlet 生命周期非常相似,下面给出图示:

img

JSP 编译

当浏览器请求 JSP 页面时,JSP 引擎会首先去检查是否需要编译这个文件。如果这个文件没有被编译过,或者在上次编译后被更改过,则编译这个 JSP 文件。

编译的过程包括三个步骤:

  • 解析 JSP 文件。
  • 将 JSP 文件转为 servlet。
  • 编译 servlet。

JSP 初始化

容器载入 JSP 文件后,它会在为请求提供任何服务前调用 jspInit()方法。如果您需要执行自定义的 JSP 初始化任务,复写 jspInit()方法就行了,就像下面这样:

1
2
3
public void jspInit(){
// 初始化代码
}

一般来讲程序只初始化一次,servlet 也是如此。通常情况下您可以在 jspInit()方法中初始化数据库连接、打开文件和创建查询表。

JSP 执行

这一阶段描述了 JSP 生命周期中一切与请求相关的交互行为,直到被销毁。

当 JSP 网页完成初始化后,JSP 引擎将会调用 _jspService() 方法。

_jspService() 方法需要一个 HttpServletRequest 对象和一个 HttpServletResponse 对象作为它的参数,就像下面这样:

1
2
3
4
void _jspService(HttpServletRequest request,
HttpServletResponse response) {
// 服务端处理代码
}

_jspService() 方法在每个 request 中被调用一次并且负责产生与之相对应的 response,并且它还负责产生所有 7 个 HTTP 方法的回应,比如 GET、POST、DELETE 等等。

JSP 清理

JSP 生命周期的销毁阶段描述了当一个 JSP 网页从容器中被移除时所发生的一切。

jspDestroy()方法在 JSP 中等价于 servlet 中的销毁方法。当您需要执行任何清理工作时复写 jspDestroy()方法,比如释放数据库连接或者关闭文件夹等等。

jspDestroy()方法的格式如下:

1
2
3
public void jspDestroy() {
// 清理代码
}

语法

脚本

脚本程序可以包含任意量的 Java 语句、变量、方法或表达式,只要它们在脚本语言中是有效的。

脚本程序的语法格式:

1
<% 代码片段 %>

或者,您也可以编写与其等价的 XML 语句,就像下面这样:

1
2
3
<jsp:scriptlet>
代码片段
</jsp:scriptlet>

任何文本、HTML 标签、JSP 元素必须写在脚本程序的外面。

下面给出一个示例,同时也是本教程的第一个 JSP 示例:

1
2
3
4
5
6
7
8
9
<html>
<head>
<title>Hello World</title>
</head>
<body>
Hello World!<br />
<% out.println("Your IP address is " + request.getRemoteAddr()); %>
</body>
</html>

注意:请确保 Apache Tomcat 已经安装在 C:\apache-tomcat-7.0.2 目录下并且运行环境已经正确设置。

将以上代码保存在 hello.jsp 中,然后将它放置在 C:\apache-tomcat-7.0.2\webapps\ROOT 目录下,打开浏览器并在地址栏中输入 http://localhost:8080/hello.jsp 。运行后得到以下结果:

img

中文编码问题

如果我们要在页面正常显示中文,我们需要在 JSP 文件头部添加以下代码:<>

1
2
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>

接下来我们将以上程序修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>菜鸟教程(runoob.com)</title>
</head>
<body>
Hello World!<br />
<% out.println("你的 IP 地址 " + request.getRemoteAddr()); %>
</body>
</html>

这样中文就可以正常显示了。

JSP 声明

一个声明语句可以声明一个或多个变量、方法,供后面的 Java 代码使用。在 JSP 文件中,您必须先声明这些变量和方法然后才能使用它们。

JSP 声明的语法格式:

1
<%! declaration; [ declaration; ]+ ... %>

或者,您也可以编写与其等价的 XML 语句,就像下面这样:

1
2
3
<jsp:declaration>
代码片段
</jsp:declaration>

程序示例:

1
<%! int i = 0; %> <%! int a, b, c; %> <%! Circle a = new Circle(2.0); %>

JSP 表达式

一个 JSP 表达式中包含的脚本语言表达式,先被转化成 String,然后插入到表达式出现的地方。

由于表达式的值会被转化成 String,所以您可以在一个文本行中使用表达式而不用去管它是否是 HTML 标签。

表达式元素中可以包含任何符合 Java 语言规范的表达式,但是不能使用分号来结束表达式。

JSP 表达式的语法格式:

1
<%= 表达式 %>

同样,您也可以编写与之等价的 XML 语句:

1
2
3
<jsp:expression>
表达式
</jsp:expression>

程序示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>菜鸟教程(runoob.com)</title>
</head>
<body>
<p>
今天的日期是: <%= (new java.util.Date()).toLocaleString()%>
</p>
</body>
</html>

运行后得到以下结果:

1
今天的日期是: 2016-6-25 13:40:07

JSP 注释

JSP 注释主要有两个作用:为代码作注释以及将某段代码注释掉。

JSP 注释的语法格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>JSP注释示例</title>
</head>
<body>
<%-- 该部分注释在网页中不会被显示--%>
<p>
今天的日期是: <%= (new java.util.Date()).toLocaleString()%>
</p>
</body>
</html>

运行后得到以下结果:

1
今天的日期是: 2016-6-25 13:41:26

不同情况下使用注释的语法规则:

语法 描述
<%-- 注释 --%> JSP 注释,注释内容不会被发送至浏览器甚至不会被编译
<!-- 注释 --> HTML 注释,通过浏览器查看网页源代码时可以看见注释内容
<% 代表静态 <% 常量
%> 代表静态 %> 常量
' 在属性中使用的单引号
" 在属性中使用的双引号

控制语句

JSP 提供对 Java 语言的全面支持。您可以在 JSP 程序中使用 Java API 甚至建立 Java 代码块,包括判断语句和循环语句等等。

if…else 语句

If…else块,请看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%> <%! int day = 1; %>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>02.JSP语法 - if...else示例</title>
</head>
<body>
<h3>IF...ELSE 实例</h3>
<% if (day == 1 | day == 7) { %>
<p>今天是周末</p>
<% } else { %>
<p>今天不是周末</p>
<% } %>
</body>
</html>

运行后得到以下结果:

1
2
IF...ELSE 实例
今天不是周末

switch…case 语句

现在来看看 switch…case 块,与 if…else 块有很大的不同,它使用 out.println(),并且整个都装在脚本程序的标签中,就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%> <%! int day = 3; %>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>02.JSP语法 - switch...case示例</title>
</head>
<body>
<h3>Sswitch...case示例</h3>
<% switch(day) { case 0: out.println("星期天"); break; case 1:
out.println("星期一"); break; case 2: out.println("星期二"); break; case 3:
out.println("星期三"); break; case 4: out.println("星期四"); break; case 5:
out.println("星期五"); break; default: out.println("星期六"); } %>
</body>
</html>

浏览器访问,运行后得出以下结果:

1
2
3
SWITCH...CASE 实例

星期三

循环语句

在 JSP 程序中可以使用 Java 的三个基本循环类型:for,while,和 do…while。

让我们来看看 for 循环的例子,以下输出的不同字体大小的”菜鸟教程”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%> <%! int fontSize; %>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>菜鸟教程(runoob.com)</title>
</head>
<body>
<h3>For 循环实例</h3>
<%for ( fontSize = 1; fontSize <= 3; fontSize++){ %>
<font color="green" size="<%= fontSize %>">
菜鸟教程 </font
><br />
<%}%>
</body>
</html>

运行后得到以下结果:

img

将上例改用 while 循环来写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%> <%! int fontSize; %>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>菜鸟教程(runoob.com)</title>
</head>
<body>
<h3>While 循环实例</h3>
<%while ( fontSize <= 3){ %>
<font color="green" size="<%= fontSize %>">
菜鸟教程 </font
><br />
<%fontSize++;%> <%}%>
</body>
</html>

浏览器访问,输出结果为(fontSize 初始化为 0,所以多输出了一行):

img

JSP 运算符

JSP 支持所有 Java 逻辑和算术运算符。

下表罗列出了 JSP 常见运算符,优先级从高到底:

类别 操作符 结合性
后缀 () [] . (点运算符) 左到右
一元 ++ - - ! ~ 右到左
可乘性 * / % 左到右
可加性 + - 左到右
移位 >> >>> << 左到右
关系 > >= < <= 左到右
相等/不等 == != 左到右
位与 & 左到右
位异或 ^ 左到右
位或 ` `
逻辑与 && 左到右
逻辑或 `
条件判断 ?: 右到左
赋值 `= += -= *= /= %= >>= <<= &= ^= =`
逗号 , 左到右

JSP 字面量

JSP 语言定义了以下几个字面量:

  • 布尔值(boolean):true 和 false;
  • 整型(int):与 Java 中的一样;
  • 浮点型(float):与 Java 中的一样;
  • 字符串(string):以单引号或双引号开始和结束;
  • Null:null。

指令

JSP 指令用来设置整个 JSP 页面相关的属性,如网页的编码方式和脚本语言。

JSP 指令以开<%@开始,以%>结束。

JSP 指令语法格式如下:

1
<%@ directive attribute="value" %>

指令可以有很多个属性,它们以键值对的形式存在,并用逗号隔开。

JSP 中的三种指令标签:

指令 描述
<%@ page ... %> 定义网页依赖属性,比如脚本语言、error 页面、缓存需求等等
<%@ include ... %> 包含其他文件
<%@ taglib ... %> 引入标签库的定义,可以是自定义标签

Page 指令

Page 指令为容器提供当前页面的使用说明。一个 JSP 页面可以包含多个page指令。

Page 指令的语法格式:

1
<%@ page attribute="value" %>

等价的 XML 格式:

1
<jsp:directive.page attribute="value" />

例:

1
2
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>

属性

下表列出与 Page 指令相关的属性:

属性 描述
buffer 指定 out 对象使用缓冲区的大小
autoFlush 控制 out 对象的 缓存区
contentType 指定当前 JSP 页面的 MIME 类型和字符编码
errorPage 指定当 JSP 页面发生异常时需要转向的错误处理页面
isErrorPage 指定当前页面是否可以作为另一个 JSP 页面的错误处理页面
extends 指定 servlet 从哪一个类继承
import 导入要使用的 Java 类
info 定义 JSP 页面的描述信息
isThreadSafe 指定对 JSP 页面的访问是否为线程安全
language 定义 JSP 页面所用的脚本语言,默认是 Java
session 指定 JSP 页面是否使用 session
isELIgnored 指定是否执行 EL 表达式
isScriptingEnabled 确定脚本元素能否被使用

Include 指令

JSP 可以通过include指令来包含其他文件。

被包含的文件可以是 JSP 文件、HTML 文件或文本文件。包含的文件就好像是该 JSP 文件的一部分,会被同时编译执行。

Include 指令的语法格式如下:

1
<%@ include file="文件相对 url 地址" %>

include 指令中的文件名实际上是一个相对的 URL 地址。

如果您没有给文件关联一个路径,JSP 编译器默认在当前路径下寻找。

等价的 XML 语法:

1
<jsp:directive.include file="文件相对 url 地址" />

Taglib 指令

JSP 允许用户自定义标签,一个自定义标签库就是自定义标签的集合。

taglib指令引入一个自定义标签集合的定义,包括库路径、自定义标签。

taglib指令的语法:

1
<%@ taglib uri="uri" prefix="prefixOfTag" %>

uri 属性确定标签库的位置,prefix 属性指定标签库的前缀。

等价的 XML 语法:

1
<jsp:directive.taglib uri="uri" prefix="prefixOfTag" />

JSP 动作元素

JSP 动作元素是一组 JSP 内置的标签,只需要书写很少的标记代码就能使用 JSP 提供的丰富功能。JSP 动作元素是对常用的 JSP 功能的抽象与封装,包括两种,自定义 JSP 动作元素与标准 JSP 动作元素。

与 JSP 指令元素不同的是,JSP 动作元素在请求处理阶段起作用。JSP 动作元素是用 XML 语法写成的。

利用 JSP 动作可以动态地插入文件、重用 JavaBean 组件、把用户重定向到另外的页面、为 Java 插件生成 HTML 代码。

动作元素只有一种语法,它符合 XML 标准:

1
<jsp:action_name attribute="value" />

动作元素基本上都是预定义的函数,JSP 规范定义了一系列的标准动作,它用 JSP 作为前缀,可用的标准动作元素如下:

语法 描述
jsp:include 在页面被请求的时候引入一个文件。
jsp:useBean 寻找或者实例化一个 JavaBean。
jsp:setProperty 设置 JavaBean 的属性。
jsp:getProperty 输出某个 JavaBean 的属性。
jsp:forward 把请求转到一个新的页面。
jsp:plugin 根据浏览器类型为 Java 插件生成 OBJECT 或 EMBED 标记。
jsp:element 定义动态 XML 元素
jsp:attribute 设置动态定义的 XML 元素属性。
jsp:body 设置动态定义的 XML 元素内容。
jsp:text 在 JSP 页面和文档中使用写入文本的模板

常见的属性

所有的动作要素都有两个属性:id 属性和 scope 属性。

  • id 属性:id 属性是动作元素的唯一标识,可以在 JSP 页面中引用。动作元素创建的 id 值可以通过 PageContext 来调用。
  • scope 属性:该属性用于识别动作元素的生命周期。 id 属性和 scope 属性有直接关系,scope 属性定义了相关联 id 对象的寿命。 scope 属性有四个可能的值: (a) page, (b)request, (c)session, 和 (d) application。

<jsp:include>

<jsp:include> 用来包含静态和动态的文件。该动作把指定文件插入正在生成的页面。

如果被包含的文件为 JSP 程序,则会先执行 JSP 程序,再将执行结果包含进来。

语法格式如下:

1
<jsp:include page="相对 URL 地址" flush="true" />

前面已经介绍过 include 指令,它是在 JSP 文件被转换成 Servlet 的时候引入文件,而这里的 jsp:include 动作不同,插入文件的时间是在页面被请求的时候。

以下是 include 动作相关的属性列表。

属性 描述
page 包含在页面中的相对 URL 地址。
flush 布尔属性,定义在包含资源前是否刷新缓存区。

示例:

以下我们定义了两个文件 date.jspmain.jsp,代码如下所示:

date.jsp 文件代码:

1
2
3
4
5
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<p>
今天的日期是: <%= (new java.util.Date())%>
</p>

main.jsp 文件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>菜鸟教程(runoob.com)</title>
</head>
<body>
<h2>include 动作实例</h2>
<jsp:include page="date.jsp" flush="true" />
</body>
</html>

现在将以上两个文件放在服务器的根目录下,访问 main.jsp 文件。显示结果如下:

1
2
3
include 动作实例

今天的日期是: 2016-6-25 14:08:17

<jsp:useBean>

jsp:useBean 动作用来加载一个将在 JSP 页面中使用的 JavaBean。

这个功能非常有用,因为它使得我们可以发挥 Java 组件复用的优势。

jsp:useBean 动作最简单的语法为:

1
<jsp:useBean id="name" class="package.class" />

在类载入后,我们既可以通过 jsp:setProperty 和 jsp:getProperty 动作来修改和检索 bean 的属性。

以下是 useBean 动作相关的属性列表。

属性 描述
class 指定 Bean 的完整包名。
type 指定将引用该对象变量的类型。
beanName 通过 java.beans.Beans 的 instantiate() 方法指定 Bean 的名字。

在给出具体实例前,让我们先来看下 jsp:setProperty 和 jsp:getProperty 动作元素:

<jsp:setProperty>

jsp:setProperty 用来设置已经实例化的 Bean 对象的属性,有两种用法。首先,你可以在 jsp:useBean 元素的外面(后面)使用 jsp:setProperty,如下所示:

1
2
3
<jsp:useBean id="myName" ... />
...
<jsp:setProperty name="myName" property="someProperty" .../>

此时,不管 jsp:useBean 是找到了一个现有的 Bean,还是新创建了一个 Bean 实例,jsp:setProperty 都会执行。第二种用法是把 jsp:setProperty 放入 jsp:useBean 元素的内部,如下所示:

1
2
3
4
<jsp:useBean id="myName" ... >
...
<jsp:setProperty name="myName" property="someProperty" .../>
</jsp:useBean>

此时,jsp:setProperty 只有在新建 Bean 实例时才会执行,如果是使用现有实例则不执行 jsp:setProperty。

jsp:setProperty 动作有下面四个属性,如下表:

属性 描述
name name 属性是必需的。它表示要设置属性的是哪个 Bean。
property property 属性是必需的。它表示要设置哪个属性。有一个特殊用法:如果 property 的值是”*“,表示所有名字和 Bean 属性名字匹配的请求参数都将被传递给相应的属性 set 方法。
value value 属性是可选的。该属性用来指定 Bean 属性的值。字符串数据会在目标类中通过标准的 valueOf 方法自动转换成数字、boolean、Boolean、 byte、Byte、char、Character。例如,boolean 和 Boolean 类型的属性值(比如”true”)通过 Boolean.valueOf 转换,int 和 Integer 类型的属性值(比如”42”)通过 Integer.valueOf 转换。    value 和 param 不能同时使用,但可以使用其中任意一个。
param param 是可选的。它指定用哪个请求参数作为 Bean 属性的值。如果当前请求没有参数,则什么事情也不做,系统不会把 null 传递给 Bean 属性的 set 方法。因此,你可以让 Bean 自己提供默认属性值,只有当请求参数明确指定了新值时才修改默认属性值。

<jsp:getProperty>

jsp:getProperty 动作提取指定 Bean 属性的值,转换成字符串,然后输出。语法格式如下:

1
2
3
<jsp:useBean id="myName" ... />
...
<jsp:getProperty name="myName" property="someProperty" .../>

下表是与 getProperty 相关联的属性:

属性 描述
name 要检索的 Bean 属性名称。Bean 必须已定义。
property 表示要提取 Bean 属性的值

实例

以下实例我们使用了 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
package com.runoob.main;

public class TestBean {
private String message = "菜鸟教程";

public String getMessage() {
return(message);
}
public void setMessage(String message) {
this.message = message;
}
}

编译以上实例文件 TestBean.java :

1
$ javac TestBean.java

编译完成后会在当前目录下生成一个 TestBean.class 文件, 将该文件拷贝至当前 JSP 项目的 WebContent/WEB-INF/classes/com/runoob/main 下( com/runoob/main 包路径,没有需要手动创建)。

下面是一个 Eclipse 中目录结构图:

img

下面是一个很简单的例子,它的功能是装载一个 Bean,然后设置/读取它的 message 属性。

现在让我们在 main.jsp 文件中调用该 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>菜鸟教程(runoob.com)</title>
</head>
<body>
<h2>Jsp 使用 JavaBean 实例</h2>
<jsp:useBean id="test" class="com.runoob.main.TestBean" />

<jsp:setProperty name="test" property="message" value="菜鸟教程..." />

<p>输出信息....</p>

<jsp:getProperty name="test" property="message" />
</body>
</html>

浏览器访问,执行以上文件,输出如下所示:

img

<jsp:forward>

jsp:forward 动作把请求转到另外的页面。jsp:forward 标记只有一个属性 page。语法格式如下所示:

1
<jsp:forward page="相对 URL 地址" />

以下是 forward 相关联的属性:

属性 描述
page page 属性包含的是一个相对 URL。page 的值既可以直接给出,也可以在请求的时候动态计算,可以是一个 JSP 页面或者一个 Java Servlet.

实例

以下实例我们使用了两个文件,分别是: date.jsp 和 main.jsp。

date.jsp 文件代码如下:

1
2
3
4
5
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<p>
今天的日期是: <%= (new java.util.Date()).toLocaleString()%>
</p>

main.jsp 文件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>菜鸟教程(runoob.com)</title>
</head>
<body>
<h2>forward 动作实例</h2>
<jsp:forward page="date.jsp" />
</body>
</html>

现在将以上两个文件放在服务器的根目录下,访问 main.jsp 文件。显示结果如下:

1
今天的日期是: 2016-6-25 14:37:25

<jsp:plugin>

jsp:plugin 动作用来根据浏览器的类型,插入通过 Java 插件 运行 Java Applet 所必需的 OBJECT 或 EMBED 元素。

如果需要的插件不存在,它会下载插件,然后执行 Java 组件。 Java 组件可以是一个 applet 或一个 JavaBean。

plugin 动作有多个对应 HTML 元素的属性用于格式化 Java 组件。param 元素可用于向 Applet 或 Bean 传递参数。

以下是使用 plugin 动作元素的典型实例:

1
2
3
4
5
6
7
8
9
10
<jsp:plugin type="applet" codebase="dirname" code="MyApplet.class"
width="60" height="80">
<jsp:param name="fontcolor" value="red" />
<jsp:param name="background" value="black" />

<jsp:fallback>
Unable to initialize Java Plugin
</jsp:fallback>

</jsp:plugin>

如果你有兴趣可以尝试使用 applet 来测试 jsp:plugin 动作元素,<fallback> 元素是一个新元素,在组件出现故障的错误是发送给用户错误信息。

<jsp:element><jsp:attribute><jsp:body>

<jsp:element><jsp:attribute><jsp:body> 动作元素动态定义 XML 元素。动态是非常重要的,这就意味着 XML 元素在编译时是动态生成的而非静态。

以下实例动态定义了 XML 元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>菜鸟教程(runoob.com)</title>
</head>
<body>
<jsp:element name="xmlElement">
<jsp:attribute name="xmlElementAttr">
属性值
</jsp:attribute>
<jsp:body>
XML 元素的主体
</jsp:body>
</jsp:element>
</body>
</html>

浏览器访问以下页面,输出结果如下所示:

img

<jsp:text>

jsp:text动作元素允许在 JSP 页面和文档中使用写入文本的模板,语法格式如下:

1
<jsp:text>模板数据</jsp:text>

以上文本模板不能包含其他元素,只能只能包含文本和 EL 表达式(注:EL 表达式将在后续章节中介绍)。请注意,在 XML 文件中,您不能使用表达式如 ${whatever > 0},因为>符号是非法的。 你可以使用 ${whatever gt 0}表达式或者嵌入在一个 CDATA 部分的值。

1
<jsp:text><![CDATA[<br>]]></jsp:text>

如果你需要在 XHTML 中声明 DOCTYPE,必须使用到 <jsp:text> 动作元素,实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<jsp:text><![CDATA[<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"DTD/xhtml1-strict.dtd">]]>
</jsp:text>
<head><title>jsp:text action</title></head>
<body>

<books><book><jsp:text>
Welcome to JSP Programming
</jsp:text></book></books>

</body>
</html>

你可以对以上实例尝试使用jsp:text及不使用该动作元素执行结果的区别。

JSP 隐式对象

JSP 隐式对象是 JSP 容器为每个页面提供的 Java 对象,开发者可以直接使用它们而不用显式声明。JSP 隐式对象也被称为预定义变量。

JSP 所支持的九大隐式对象:

对象 描述
request HttpServletRequest类的实例
response HttpServletResponse类的实例
out PrintWriter类的实例,用于把结果输出至网页上
session HttpSession类的实例
application ServletContext类的实例,与应用上下文有关
config ServletConfig类的实例
pageContext PageContext类的实例,提供对 JSP 页面所有对象以及命名空间的访问
page 类似于 Java 类中的 this 关键字
Exception Exception类的对象,代表发生错误的 JSP 页面中对应的异常对象

request 对象

request对象是javax.servlet.http.HttpServletRequest 类的实例。

每当客户端请求一个 JSP 页面时,JSP 引擎就会制造一个新的request对象来代表这个请求。

request对象提供了一系列方法来获取 HTTP 头信息,cookies,HTTP 方法等等。

response 对象

response对象是javax.servlet.http.HttpServletResponse类的实例。

当服务器创建request对象时会同时创建用于响应这个客户端的response对象。

response对象也定义了处理 HTTP 头模块的接口。通过这个对象,开发者们可以添加新的 cookies,时间戳,HTTP 状态码等等。

out 对象

out对象是javax.servlet.jsp.JspWriter类的实例,用来在response对象中写入内容。

最初的JspWriter类对象根据页面是否有缓存来进行不同的实例化操作。可以在page指令中使用buffered='false'属性来轻松关闭缓存。

JspWriter类包含了大部分java.io.PrintWriter类中的方法。不过,JspWriter新增了一些专为处理缓存而设计的方法。还有就是,JspWriter类会抛出IOExceptions异常,而PrintWriter不会。

下表列出了我们将会用来输出booleancharintdoubleStringobject等类型数据的重要方法:

方法 描述
out.print(dataType dt) 输出 Type 类型的值
out.println(dataType dt) 输出 Type 类型的值然后换行
out.flush() 刷新输出流

session 对象

session对象是javax.servlet.http.HttpSession类的实例。和 Java Servlets 中的session对象有一样的行为。

session对象用来跟踪在各个客户端请求间的会话。

application 对象

application对象直接包装了 servlet 的ServletContext类的对象,是javax.servlet.ServletContext类的实例。

这个对象在 JSP 页面的整个生命周期中都代表着这个 JSP 页面。这个对象在 JSP 页面初始化时被创建,随着jspDestroy()方法的调用而被移除。

通过向application中添加属性,则所有组成您 web 应用的 JSP 文件都能访问到这些属性。

config 对象

config对象是javax.servlet.ServletConfig类的实例,直接包装了 servlet 的ServletConfig类的对象。

这个对象允许开发者访问 Servlet 或者 JSP 引擎的初始化参数,比如文件路径等。

以下是 config 对象的使用方法,不是很重要,所以不常用:

1
config.getServletName();

它返回包含在<servlet-name>元素中的 servlet 名字,注意,<servlet-name>元素在WEB-INF\web.xml文件中定义。

pageContext 对象

pageContext对象是javax.servlet.jsp.PageContext类的实例,用来代表整个 JSP 页面。

这个对象主要用来访问页面信息,同时过滤掉大部分实现细节。

这个对象存储了request对象和response对象的引用。application对象,config对象,session对象,out对象可以通过访问这个对象的属性来导出。

pageContext对象也包含了传给 JSP 页面的指令信息,包括缓存信息,ErrorPage URL,页面 scope 等。

PageContext类定义了一些字段,包括 PAGE_SCOPE,REQUEST_SCOPE,SESSION_SCOPE, APPLICATION_SCOPE。它也提供了 40 余种方法,有一半继承自javax.servlet.jsp.JspContext 类。

其中一个重要的方法就是removeArribute(),它可接受一个或两个参数。比如,pageContext.removeArribute(“attrName”)移除四个 scope 中相关属性,但是下面这种方法只移除特定 scope 中的相关属性:

1
pageContext.removeAttribute("attrName", PAGE_SCOPE);

page 对象

这个对象就是页面实例的引用。它可以被看做是整个 JSP 页面的代表。

page对象就是this对象的同义词。

exception 对象

exception对象包装了从先前页面中抛出的异常信息。它通常被用来产生对出错条件的适当响应。

EL 表达式

EL 表达式是用${}括起来的脚本,用来更方便地读取对象。EL 表达式写在 JSP 的 HTML 代码中,而不能写在<%%>引起的 JSP 脚本中。

JSP 表达式语言(EL)使得访问存储在 JavaBean 中的数据变得非常简单。JSP EL 既可以用来创建算术表达式也可以用来创建逻辑表达式。在 JSP EL 表达式内可以使用整型数,浮点数,字符串,常量 true、false,还有 null。

一个简单的语法

典型的,当您需要在 JSP 标签中指定一个属性值时,只需要简单地使用字符串即可:

1
<jsp:setProperty name="box" property="perimeter" value="100" />

JSP EL 允许您指定一个表达式来表示属性值。一个简单的表达式语法如下:

1
${expr}

其中,expr 指的是表达式。在 JSP EL 中通用的操作符是”.”和”[]”。这两个操作符允许您通过内嵌的 JSP 对象访问各种各样的 JavaBean 属性。

举例来说,上面的 <jsp:setProperty> 标签可以使用表达式语言改写成如下形式:

1
2
3
4
5
<jsp:setProperty
name="box"
property="perimeter"
value="${2*box.width+2*box.height}"
/>

当 JSP 编译器在属性中见到”${}”格式后,它会产生代码来计算这个表达式,并且产生一个替代品来代替表达式的值。

您也可以在标签的模板文本中使用表达式语言。比如 <jsp:text> 标签简单地将其主体中的文本插入到 JSP 输出中:

1
2
3
<jsp:text>
<h1>Hello JSP!</h1>
</jsp:text>

现在,在jsp:text标签主体中使用表达式,就像这样:

1
2
3
<jsp:text>
Box Perimeter is: ${2*box.width + 2*box.height}
</jsp:text>

在 EL 表达式中可以使用圆括号来组织子表达式。比如 ${(1 + 2) _ 3} 等于 9,但是 ${1 + (2 _ 3)} 等于 7。

想要停用对 EL 表达式的评估的话,需要使用 page 指令将 isELIgnored 属性值设为 true:

1
<%@ page isELIgnored ="true|false" %>

这样,EL 表达式就会被忽略。若设为 false,则容器将会计算 EL 表达式。

EL 中的基础操作符

EL 表达式支持大部分 Java 所提供的算术和逻辑操作符:

操作符 描述
. 访问一个 Bean 属性或者一个映射条目
[] 访问一个数组或者链表的元素
( ) 组织一个子表达式以改变优先级
+
- 减或负
*
/ or div
% or mod 取模
== or eq 测试是否相等
!= or ne 测试是否不等
< or lt 测试是否小于
> or gt 测试是否大于
<= or le 测试是否小于等于
>= or ge 测试是否大于等于
&& or and 测试逻辑与
|| or or 测试逻辑或
! or not 测试取反
empty 测试是否空值

JSP EL 中的函数

JSP EL 允许您在表达式中使用函数。这些函数必须被定义在自定义标签库中。函数的使用语法如下:

1
${ns:func(param1, param2, ...)}

ns 指的是命名空间(namespace),func 指的是函数的名称,param1 指的是第一个参数,param2 指的是第二个参数,以此类推。比如,有函数 fn:length,在 JSTL 库中定义,可以像下面这样来获取一个字符串的长度:

1
${fn:length("Get my length")}

要使用任何标签库中的函数,您需要将这些库安装在服务器中,然后使用 <taglib> 标签在 JSP 文件中包含这些库。

JSP EL 隐含对象

JSP EL 支持下表列出的隐含对象:

隐含对象 描述
pageScope page 作用域
requestScope request 作用域
sessionScope session 作用域
applicationScope application 作用域
param Request 对象的参数,字符串
paramValues Request 对象的参数,字符串集合
header HTTP 信息头,字符串
headerValues HTTP 信息头,字符串集合
initParam 上下文初始化参数
cookie Cookie 值
pageContext 当前页面的 pageContext

您可以在表达式中使用这些对象,就像使用变量一样。接下来会给出几个例子来更好的理解这个概念。

pageContext 对象

pageContext 对象是 JSP 中 pageContext 对象的引用。通过 pageContext 对象,您可以访问 request 对象。比如,访问 request 对象传入的查询字符串,就像这样:

1
${pageContext.request.queryString}

Scope 对象

pageScope,requestScope,sessionScope,applicationScope 变量用来访问存储在各个作用域层次的变量。

举例来说,如果您需要显式访问在 applicationScope 层的 box 变量,可以这样来访问:applicationScope.box。

param 和 paramValues 对象

param 和 paramValues 对象用来访问参数值,通过使用 request.getParameter 方法和 request.getParameterValues 方法。

举例来说,访问一个名为 order 的参数,可以这样使用表达式:${param.order},或者${param["order"]}

接下来的例子表明了如何访问 request 中的 username 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page import="java.io.*,java.util.*" %> <% String title = "Accessing Request
Param"; %>
<html>
<head>
<title><% out.print(title); %></title>
</head>
<body>
<center>
<h1><% out.print(title); %></h1>
</center>
<div align="center">
<p>${param["username"]}</p>
</div>
</body>
</html>

param 对象返回单一的字符串,而 paramValues 对象则返回一个字符串数组。

header 和 headerValues 对象

header 和 headerValues 对象用来访问信息头,通过使用 request.getHeader 方法和 request.getHeaders 方法。

举例来说,要访问一个名为 user-agent 的信息头,可以这样使用表达式:${header.user-agent},或者 ${header["user-agent"]}

接下来的例子表明了如何访问 user-agent 信息头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page import="java.io.*,java.util.*" %> <% String title = "User Agent
Example"; %>
<html>
<head>
<title><% out.print(title); %></title>
</head>
<body>
<center>
<h1><% out.print(title); %></h1>
</center>
<div align="center">
<p>${header["user-agent"]}</p>
</div>
</body>
</html>

运行结果如下:

img

header 对象返回单一值,而 headerValues 则返回一个字符串数组。

JSTL

JSP 标准标签库(JSTL)是一个 JSP 标签集合,它封装了 JSP 应用的通用核心功能。

JSTL 支持通用的、结构化的任务,比如迭代,条件判断,XML 文档操作,国际化标签,SQL 标签。 除了这些,它还提供了一个框架来使用集成 JSTL 的自定义标签。

根据 JSTL 标签所提供的功能,可以将其分为 5 个类别。

  • 核心标签
  • 格式化标签
  • SQL 标签
  • XML 标签
  • JSTL 函数

JSTL 库安装

Apache Tomcat 安装 JSTL 库步骤如下:

从 Apache 的标准标签库中下载的二进包(jakarta-taglibs-standard-current.zip)。

下载 jakarta-taglibs-standard-1.1.2.zip 包并解压,将 jakarta-taglibs-standard-1.1.2/lib/ 下的两个 jar 文件:standard.jarjstl.jar 文件拷贝到 /WEB-INF/lib/ 下。

将 tld 下的需要引入的 tld 文件复制到 WEB-INF 目录下。

接下来我们在 web.xml 文件中添加以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?xml version="1.0" encoding="UTF-8"?>
<web-app
version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
>
<jsp-config>
<taglib>
<taglib-uri>http://java.sun.com/jsp/jstl/fmt</taglib-uri>
<taglib-location>/WEB-INF/fmt.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>http://java.sun.com/jsp/jstl/fmt-rt</taglib-uri>
<taglib-location>/WEB-INF/fmt-rt.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>http://java.sun.com/jsp/jstl/core</taglib-uri>
<taglib-location>/WEB-INF/c.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>http://java.sun.com/jsp/jstl/core-rt</taglib-uri>
<taglib-location>/WEB-INF/c-rt.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>http://java.sun.com/jsp/jstl/sql</taglib-uri>
<taglib-location>/WEB-INF/sql.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>http://java.sun.com/jsp/jstl/sql-rt</taglib-uri>
<taglib-location>/WEB-INF/sql-rt.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>http://java.sun.com/jsp/jstl/x</taglib-uri>
<taglib-location>/WEB-INF/x.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>http://java.sun.com/jsp/jstl/x-rt</taglib-uri>
<taglib-location>/WEB-INF/x-rt.tld</taglib-location>
</taglib>
</jsp-config>
</web-app>

使用任何库,你必须在每个 JSP 文件中的头部包含 <taglib> 标签。

核心标签

核心标签是最常用的 JSTL 标签。引用核心标签库的语法如下:

1
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
标签 描述
<c:out> 用于在 JSP 中显示数据,就像<%= … >
<c:set> 用于保存数据
<c:remove> 用于删除数据
<c:catch> 用来处理产生错误的异常状况,并且将错误信息储存起来
<c:if> 与我们在一般程序中用的 if 一样
<c:choose> 本身只当做 <c:when><c:otherwise> 的父标签
<c:when> <c:choose> 的子标签,用来判断条件是否成立
<c:otherwise> <c:choose> 的子标签,接在 <c:when> 标签后,当 <c:when> 标签判断为 false 时被执行
<c:import> 检索一个绝对或相对 URL,然后将其内容暴露给页面
<c:forEach> 基础迭代标签,接受多种集合类型
<c:forTokens> 根据指定的分隔符来分隔内容并迭代输出
<c:param> 用来给包含或重定向的页面传递参数
<c:redirect> 重定向至一个新的 URL.
<c:url> 使用可选的查询参数来创造一个 URL

格式化标签

JSTL 格式化标签用来格式化并输出文本、日期、时间、数字。引用格式化标签库的语法如下:

1
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
标签 描述
<fmt:formatNumber> 使用指定的格式或精度格式化数字
<fmt:parseNumber> 解析一个代表着数字,货币或百分比的字符串
<fmt:formatDate> 使用指定的风格或模式格式化日期和时间
<fmt:parseDate> 解析一个代表着日期或时间的字符串
<fmt:bundle> 绑定资源
<fmt:setLocale> 指定地区
<fmt:setBundle> 绑定资源
<fmt:timeZone> 指定时区
<fmt:setTimeZone> 指定时区
<fmt:message> 显示资源配置文件信息
<fmt:requestEncoding> 设置 request 的字符编码

SQL 标签

JSTL SQL 标签库提供了与关系型数据库(Oracle,MySQL,SQL Server 等等)进行交互的标签。引用 SQL 标签库的语法如下:

1
<%@ taglib prefix="sql" uri="http://java.sun.com/jsp/jstl/sql" %>
标签 描述
<sql:setDataSource> 指定数据源
<sql:query> 运行 SQL 查询语句
<sql:update> 运行 SQL 更新语句
<sql:param> 将 SQL 语句中的参数设为指定值
<sql:dateParam> 将 SQL 语句中的日期参数设为指定的 java.util.Date 对象值
<sql:transaction> 在共享数据库连接中提供嵌套的数据库行为元素,将所有语句以一个事务的形式来运行

XML 标签

JSTL XML 标签库提供了创建和操作 XML 文档的标签。引用 XML 标签库的语法如下:

1
<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %>

在使用 xml 标签前,你必须将 XML 和 XPath 的相关包拷贝至你的 <Tomcat 安装目录>\lib 下:

标签 描述
<x:out> <%= ... >,类似,不过只用于 XPath 表达式
<x:parse> 解析 XML 数据
<x:set> 设置 XPath 表达式
<x:if> 判断 XPath 表达式,若为真,则执行本体中的内容,否则跳过本体
<x:forEach> 迭代 XML 文档中的节点
<x:choose> <x:when><x:otherwise> 的父标签
<x:when> <x:choose> 的子标签,用来进行条件判断
<x:otherwise> <x:choose> 的子标签,当 <x:when> 判断为 false 时被执行
<x:transform> 将 XSL 转换应用在 XML 文档中
<x:param> <x:transform> 共同使用,用于设置 XSL 样式表

JSTL 函数

JSTL 包含一系列标准函数,大部分是通用的字符串处理函数。引用 JSTL 函数库的语法如下:

1
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
函数 描述
fn:contains() 测试输入的字符串是否包含指定的子串
fn:containsIgnoreCase() 测试输入的字符串是否包含指定的子串,大小写不敏感
fn:endsWith() 测试输入的字符串是否以指定的后缀结尾
fn:escapeXml() 跳过可以作为 XML 标记的字符
fn:indexOf() 返回指定字符串在输入字符串中出现的位置
fn:join() 将数组中的元素合成一个字符串然后输出
fn:length() 返回字符串长度
fn:replace() 将输入字符串中指定的位置替换为指定的字符串然后返回
fn:split() 将字符串用指定的分隔符分隔然后组成一个子字符串数组并返回
fn:startsWith() 测试输入字符串是否以指定的前缀开始
fn:substring() 返回字符串的子集
fn:substringAfter() 返回字符串在指定子串之后的子集
fn:substringBefore() 返回字符串在指定子串之前的子集
fn:toLowerCase() 将字符串中的字符转为小写
fn:toUpperCase() 将字符串中的字符转为大写
fn:trim() 移除首尾的空白符

Taglib

JSP 自定义标签

自定义标签是用户定义的 JSP 语言元素。当 JSP 页面包含一个自定义标签时将被转化为 servlet,标签转化为对被 称为 tag handler 的对象的操作,即当 servlet 执行时 Web container 调用那些操作。

JSP 标签扩展可以让你创建新的标签并且可以直接插入到一个 JSP 页面。 JSP 2.0 规范中引入 Simple Tag Handlers 来编写这些自定义标记。

你可以继承 SimpleTagSupport 类并重写的 doTag()方法来开发一个最简单的自定义标签。

创建”Hello”标签

接下来,我们想创建一个自定义标签叫作ex:Hello,标签格式为:

1
<ex:Hello />

要创建自定义的 JSP 标签,你首先必须创建处理标签的 Java 类。所以,让我们创建一个 HelloTag 类,如下所示:

1
2
3
4
package com.runoob; import javax.servlet.jsp.tagext.*; import
javax.servlet.jsp.*; import java.io.*; public class HelloTag extends
SimpleTagSupport { public void doTag() throws JspException, IOException {
JspWriter out = getJspContext().getOut(); out.println("Hello Custom Tag!"); } }

以下代码重写了 doTag()方法,方法中使用了 getJspContext()方法来获取当前的 JspContext 对象,并将”Hello Custom Tag!”传递给 JspWriter 对象。

编译以上类,并将其复制到环境变量 CLASSPATH 目录中。最后创建如下标签库:<Tomcat安装目录>webapps\ROOT\WEB-INF\custom.tld

1
2
3
4
5
6
7
8
9
10
<taglib>
<tlib-version>1.0</tlib-version>
<jsp-version>2.0</jsp-version>
<short-name>Example TLD</short-name>
<tag>
<name>Hello</name>
<tag-class>com.runoob.HelloTag</tag-class>
<body-content>empty</body-content>
</tag>
</taglib>

接下来,我们就可以在 JSP 文件中使用 Hello 标签:

1
2
3
4
5
6
7
8
9
<%@ taglib prefix="ex" uri="WEB-INF/custom.tld"%>
<html>
<head>
<title>A sample custom tag</title>
</head>
<body>
<ex:Hello />
</body>
</html>

以上程序输出结果为:

1
Hello Custom Tag!

访问标签体

你可以像标准标签库一样在标签中包含消息内容。如我们要在我们自定义的 Hello 中包含内容,格式如下:

1
2
3
<ex:Hello>
This is message body
</ex:Hello>

我们可以修改标签处理类文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.runoob;

import javax.servlet.jsp.tagext.*;
import javax.servlet.jsp.*;
import java.io.*;

public class HelloTag extends SimpleTagSupport {

StringWriter sw = new StringWriter();
public void doTag()
throws JspException, IOException
{
getJspBody().invoke(sw);
getJspContext().getOut().println(sw.toString());
}

}

接下来我们需要修改 TLD 文件,如下所示:

1
2
3
4
5
6
7
8
9
10
<taglib>
<tlib-version>1.0</tlib-version>
<jsp-version>2.0</jsp-version>
<short-name>Example TLD with Body</short-name>
<tag>
<name>Hello</name>
<tag-class>com.runoob.HelloTag</tag-class>
<body-content>scriptless</body-content>
</tag>
</taglib>

现在我们可以在 JSP 使用修改后的标签,如下所示:

1
2
3
4
5
6
7
8
9
10
11
<%@ taglib prefix="ex" uri="WEB-INF/custom.tld"%>
<html>
<head>
<title>A sample custom tag</title>
</head>
<body>
<ex:Hello>
This is message body
</ex:Hello>
</body>
</html>

以上程序输出结果如下所示:

1
This is message body

自定义标签属性

你可以在自定义标准中设置各种属性,要接收属性,值自定义标签类必须实现 setter 方法, JavaBean 中的 setter 方法如下所示:

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
package com.runoob;

import javax.servlet.jsp.tagext.*;
import javax.servlet.jsp.*;
import java.io.*;

public class HelloTag extends SimpleTagSupport {

private String message;

public void setMessage(String msg) {
this.message = msg;
}

StringWriter sw = new StringWriter();

public void doTag()
throws JspException, IOException
{
if (message != null) {
/* 从属性中使用消息 */
JspWriter out = getJspContext().getOut();
out.println( message );
}
else {
/* 从内容体中使用消息 */
getJspBody().invoke(sw);
getJspContext().getOut().println(sw.toString());
}
}

}

属性的名称是”message”,所以 setter 方法是的 setMessage()。现在让我们在 TLD 文件中使用的 <attribute> 元素添加此属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
<taglib>
<tlib-version>1.0</tlib-version>
<jsp-version>2.0</jsp-version>
<short-name>Example TLD with Body</short-name>
<tag>
<name>Hello</name>
<tag-class>com.runoob.HelloTag</tag-class>
<body-content>scriptless</body-content>
<attribute>
<name>message</name>
</attribute>
</tag>
</taglib>

现在我们就可以在 JSP 文件中使用 message 属性了,如下所示:

1
2
3
4
5
6
7
8
9
<%@ taglib prefix="ex" uri="WEB-INF/custom.tld"%>
<html>
<head>
<title>A sample custom tag</title>
</head>
<body>
<ex:Hello message="This is custom tag" />
</body>
</html>

以上实例数据输出结果为:

1
This is custom tag

你还可以包含以下属性:

属性 描述
name 定义属性的名称。每个标签的是属性名称必须是唯一的。
required 指定属性是否是必须的或者可选的,如果设置为 false 为可选。
rtexprvalue 声明在运行表达式时,标签属性是否有效。
type 定义该属性的 Java 类类型 。默认指定为 String
description 描述信息
fragment 如果声明了该属性,属性值将被视为一个 JspFragment

以下是指定相关的属性实例:

1
2
3
4
5
6
7
8
.....
<attribute>
<name>attribute_name</name>
<required>false</required>
<type>java.util.Date</type>
<fragment>false</fragment>
</attribute>
.....

如果你使用了两个属性,修改 TLD 文件,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
.....
<attribute>
<name>attribute_name1</name>
<required>false</required>
<type>java.util.Boolean</type>
<fragment>false</fragment>
</attribute>
<attribute>
<name>attribute_name2</name>
<required>true</required>
<type>java.util.Date</type>
</attribute>
.....

JavaWeb 面经

Servlet

什么是 Servlet

Servlet(Server Applet),即小服务程序或服务连接器。Servlet 是 Java 编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态 Web 内容。

  • 狭义的 Servlet 是指 Java 实现的一个接口。
  • 广义的 Servlet 是指任何实现了这个 Servlet 接口的类。

Servlet 运行于支持 Java 的应用服务器中。从原理上讲,Servlet 可以响应任何类型的请求,但绝大多数情况下 Servlet 只用来扩展基于 HTTP 协议的 Web 服务器。

Servlet 和 CGI 的区别

Servlet 技术出现之前,Web 主要使用 CGI 技术。它们的区别如下:

  • Servlet 是基于 Java 编写的,处于服务器进程中,他能够通过多线程方式运行 service() 方法,一个实例可以服务于多个请求,而且一般不会销毁;
  • CGI(Common Gateway Interface),即通用网关接口。它会为每个请求产生新的进程,服务完成后销毁,所以效率上低于 Servlet。

Servlet 版本以及主要特性

版本 日期 JAVA EE/JDK 版本 特性
Servlet 4.0 2017 年 10 月 JavaEE 8 HTTP2
Servlet 3.1 2013 年 5 月 JavaEE 7 非阻塞 I/O,HTTP 协议升级机制
Servlet 3.0 2009 年 12 月 JavaEE 6, JavaSE 6 可插拔性,易于开发,异步 Servlet,安全性,文件上传
Servlet 2.5 2005 年 10 月 JavaEE 5, JavaSE 5 依赖 JavaSE 5,支持注解
Servlet 2.4 2003 年 11 月 J2EE 1.4, J2SE 1.3 web.xml 使用 XML Schema
Servlet 2.3 2001 年 8 月 J2EE 1.3, J2SE 1.2 Filter
Servlet 2.2 1999 年 8 月 J2EE 1.2, J2SE 1.2 成为 J2EE 标准
Servlet 2.1 1998 年 11 月 未指定 First official specification, added RequestDispatcher, ServletContext
Servlet 2.0 JDK 1.1 Part of Java Servlet Development Kit 2.0
Servlet 1.0 1997 年 6 月

Servlet 和 JSP 的区别

  1. Servlet 是一个运行在服务器上的 Java 类,依靠服务器支持向浏览器传输数据。
  2. JSP 本质上就是 Servlet,每次运行的时候 JSP 都会被编译成 .java 文件,然后再被编译成 .class 文件。
  3. 有了 JSP,Servlet 不再负责动态生成页面,转而去负责控制程序逻辑的作用,控制 JSP 与 JavaBean 之间的流转。
  4. JSP 侧重于视图,而 Servlet 侧重于控制逻辑,在 MVC 架构模式中,JSP 适合充当视图 View,Servlet 适合充当控制器 Controller。

简述 Servlet 生命周期

img

Servlet 生命周期如下:

  1. 加载 - 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器。容器通过类加载器使用 Servlet 类对应的文件加载 servlet;
  2. 初始化 - Servlet 通过调用 init () 方法进行初始化。
  3. 服务 - Servlet 调用 service() 方法来处理客户端的请求。
  4. 销毁 - Servlet 通过调用 destroy() 方法终止(结束)。
  5. 卸载 - Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。

如何现实 servlet 的单线程模式

1
<%@ page isThreadSafe="false" %>

Servlet 中如何获取用户提交的查询参数或者表单数据

  • HttpServletRequest 的 getParameter() 方法。
  • HttpServletRequest 的 getParameterValues() 方法。
  • HttpServletRequest 的 getParameterMap() 方法。

request 的主要方法

  • setAttribute(String name,Object):设置名字为 name 的 request 的参数值
  • getAttribute(String name):返回由 name 指定的属性值
  • getAttributeNames():返回 request 对象所有属性的名字集合,结果是一个枚举的实例
  • getCookies():返回客户端的所有 Cookie 对象,结果是一个 Cookie 数组
  • getCharacterEncoding():返回请求中的字符编码方式
  • getContentLength():返回请求的 Body 的长度
  • getHeader(String name):获得 HTTP 协议定义的文件头信息
  • getHeaders(String name):返回指定名字的 request Header 的所有值,结果是一个枚举的实例
  • getHeaderNames():返回所以 request Header 的名字,结果是一个枚举的实例
  • getInputStream():返回请求的输入流,用于获得请求中的数据 getMethod():获得客户端向服务器端传送数据的方法
  • getParameter(String name):获得客户端传送给服务器端的有 name 指定的参数值
  • getParameterNames():获得客户端传送给服务器端的所有参数的名字,结果是一个枚举的实例
  • getParameterValues(String name):获得有 name 指定的参数的所有值
  • getProtocol():获取客户端向服务器端传送数据所依据的协议名称
  • getQueryString():获得查询字符串
  • getRequestURI():获取发出请求字符串的客户端地址
  • getRemoteAddr():获取客户端的 IP 地址
  • getRemoteHost():获取客户端的名字
  • getSession([Boolean create]):返回和请求相关
  • Session getServerName():获取服务器的名字
  • getServletPath():获取客户端所请求的脚本文件的路径
  • getServerPort():获取服务器的端口号
  • removeAttribute(String name):删除请求中的一个属性

JSP

JSP 的内置对象

  1. request:包含客户端请求的信息
  2. response:包含服务器传回客户端的响应信息
  3. session:主要用来区分每个用户信息和会话状态
  4. pageContext:管理页面属性
  5. application:服务器启动时创建,服务器关闭时停止,保存所有应用系统中的共有数据,一个共享的内置对象(即一个容器中的多个用户共享一个 application 对象);
  6. out:向客户端输出数据
  7. config:代码片段配置对象,用于初始化 Servlet 的配置参数
  8. page:指网页本身
  9. exception:处理 JSP 文件执行时发生的错误和异常,只要在错误页面里才能使用。

JSP 的作用域

  1. page:一个页面;
  2. request:一次请求;
  3. session:一次会话;
  4. application:服务器从启动到停止。

JSP 中 7 个动作指令和作用

  1. jsp:forward - 执行页面转向,把请求转发到下一个页面;
  2. jsp:param - 用于传递参数,必须与其他支持参数的标签一起使用;
  3. jsp:include - 用于动态引入一个 JSP 页面
  4. jsp:plugin - 用于下载 JavaBean 或 Applet 到客户端执行
  5. jsp:useBean - 寻求或者实例化一个 JavaBean;
  6. jsp:setProperty - 设置 JavaBean 的属性值;
  7. jsp:getProperty - 获取 JavaBean 的属性值。

JSP 中动态 INCLUDE 和静态 INCLUDE 有什么区别

  • 静态 INCLUDE:用 include 伪码实现,不会检查所含文件的变化,适用于包含静态页面<%@ include file=”页面名称.html” %>先合并再编译
  • 动态 INCLUDE:用 jsp:include 动作实现 <jsp:include page=”页面名称 .jsp” flush=”true”> 它总是会检查文件中的变化,适用于包含动态页面,并且可以带参数先编译再合并

原理

请求转发(forward)和重定向(redirect)的区别

  • 效率上
    • 转发(forward) > 重定向(redirect)
  • 显示上
    • 重定向(redirect):显示新的 URL
    • 转发(forward):地址栏不变
  • 数据上
    • 转发(forward):可以共享 request 里面的数据
    • 重定向(redirect):不能
  • 请求次数
    • 重定向(redirect)是两次
    • 转发(forward)是一次

get 请求和 post 请求的区别

img

  • GET:
    • 从服务器上获取数据,一般不能使用在写操作接口
    • 由 URL 所限制,GET 方式传输的数据大小有所限制,传送的数据量不超过 2KB
    • 请求的数据会附加在 URL 之后,以?分隔 URL 和传输数据,多个参数用&连接
    • 安全性差
  • POST:
    • 向服务器提交数据,一般处理写业务
    • POST 方式传送的数据量比较大,一般被默认为没有限制
    • 安全性高
    • 请的求的数据内容放置在 HTML HEADER 中

用户在浏览器中输入 URL 之后,发什么了什么?写出请求和响应的流程

  1. 域名解析
  2. TCP 三次握手
  3. 浏览器向服务器发送 http 请求
  4. 浏览器发送请求头信息
  5. 服务器处理请求
  6. 服务器做出应答
  7. 服务器发送应答头信息
  8. 服务器发送数据
  9. TCP 连接关闭

什么是 Web Service?

  1. WebService 就是一个应用程序,它向外界暴露出一个能够通过 Web 进行调用的 API。
  2. 它是基于 HTTP 协议传输数据,这使得运行在不同机上的不同应用程序,无须借助附加的、专门的第三方 软件或硬件,就可以相互交换数据或集成。

会话跟踪技术有哪些?

由于 HTTP 协议本身是无状态的,服务器为了区分不同的用户,就需要对用户会话进行跟踪,简单的说就是为用户进行登记,为用户分配唯一的 ID,下一次用户在请求中包含此 ID,服务器根据此判断到底是哪一个用户。

  • URL 重写:在 URL 中添加会话信息作为请求的参数,或者将唯一的会话 ID 添加到 URL 结尾,以表示一个会话。设置表单隐藏域:将和会话跟踪相关的字段添加到隐藏域中,这些信息不会在浏览器显示,但是提交表单时会提交给服务器。
  • cookie:cookie 有两种:
    • 一种是基于窗口的,浏览器关闭后,cookie 就没有了;
    • 另一种是将信息存储在一个临时文件中,并设置其有效路径和最大存活时间。当用户通过浏览器和服务器建立一次会话后,会话 ID 就会随相应信息储存在基于窗口的 cookie 中,那就意味着只要浏览器没有关闭,会话没有超时,下一次请求时这个会话 ID 又会提交给服务器,让服务器识别用户身份。
    • 在使用 cookie 时要注意几点:
      • 首先不要在 cookie 中存放敏 感信息;
      • 其次 cookie 存储的数据量有限(4k),不能将过多的内容存储 cookie 中;
      • 再者浏览器通常只允许一个站点最多存放 20 个 cookie。
      • 当然,和用户会话相关的其他信息(除了会话 ID)也可以存在 cookie 方便进行会话 跟踪;
  • HttpSession:在所有会话跟踪技术中,HttpSession 对象是最强大也是功能最多的。当一个用户第一次访问某个网站时会自动创建 HttpSession,每个用户可以访问他自己的 HttpSession。可以通过 HttpServletRequest 对象的 getSession 方法获得 HttpSession,通过 HttpSession 的 setAttribute 方法可以将一个值放在 HttpSession 中,通过调用 HttpSession 对象的 getAttribute 方法,同时传入属性名就可以获取保存在 HttpSession 中的对象。
    • 与上面三种方式不同的是,HttpSession 放在服务器的内存中,因此不要将过大的对象放在里面,即使目前的 Servlet 容器可以在内存将满时将 HttpSession 中的对象移到其他存储设备中,但是这样势必影响性能。
    • 添加到 HttpSession 中 的值可以是任意 Java 对象,这个对象最好实现了 Serializable 接口,这样 Servlet 容器在必要的时候可以将其序列 化到文件中,否则在序列化时就会出现异常。

响应结果状态码有哪些,并给出中文含义?

  • 1**:信息性状态码
  • 2**:成功状态码
    • 200:请求正常成功
    • 204:指示请求成功但没有返回新信息
    • 206:指示服务器已完成对资源的部分 GET 请求
  • 3**:重定向状态码
    • 301:永久性重定向
    • 302:临时性重定向
    • 304:服务器端允许请求访问资源,但未满足条件
  • 4**:客户端错误状态码
    • 400:请求报文中存在语法错误
    • 401:发送的请求需要有通过 HTTP 认证的认证信息
    • 403:对请求资源的访问被服务器拒绝了
    • 404:服务器上无法找到请求的资源
  • 5**:服务器错误状态码
    • 500:服务器端在执行请求时发生了错误
    • 503:服务器暂时处于超负载或正在进行停机维护,现在无法处理请求

XML 文档定义有几种形式?它们之间有何本质区别?解析 XML 文档有哪几种方式?

(1)XML 文档有两种约束方式:

  1. DTD 约束
  2. Schema 约束

(2)XML 文档区别:
1 DTD 不符合 XML 的语法结构,schema 符合 XML 的语法结构;
2 DTD 的约束扩展性比较差,XML 文档只能引入一个 DTD 的文件。schema 可以引入多个文件;
3 DTD 不支持名称空间(理解包结构),schema 支持名称空间;
4 DTD 支持数据比较少,schema 支持更多的数据类型;

(3)解析方式主要有三种:

  • DOM 解析:
    • (a)加载整个 xml 的文档到内存中,形成树状结构,生成对象;
    • (b)容易产生内存溢出;
    • (c)可以做增删改
  • SAX 解析
    • (a)边读边解析;
    • (b)不可以做增删改
  • DOM4J 解析(hibernate 底层采用)
    • (a)可让 SAX 解析也产生树状结构。
    • (b)主要 api 开发步骤:
      • 1)SAXReader.read(xxx.xml)代表解析 xml 的文档,返回对象是 Document;
      • 2)Document.getRootElement(),返回的是文档的根节点,是 Element 对象;
      • 3)Element:
        • .element(…)– 获得指定名称第一个子元素。可以不指定名称;
        • .elements(…)– 获得指定名称的所有子元素。可以不指定名称;
        • .getText()– 获得当前元素的文本内容;
        • .elementText(…)– 获得指定名称子元素的文本值
        • .addElement()– 添加子节点
        • .setText()– 设置子标签内容
      • 4)XMLWriter.write(“..”)– 写出
      • 5)XMLWriter.close()– 关闭输出流

参考资料

SkyWalking 快速入门

SkyWalking 是一个基于 Java 开发的分布式系统的应用程序性能监视工具,专为微服务、云原生架构和基于容器(Docker、K8s、Mesos)架构而设计。

一、SkyWalking 简介

SkyWalking 是观察性分析平台和应用性能管理系统。

提供分布式追踪、服务网格遥测分析、度量聚合和可视化一体化解决方案。

img

SkyWalking 特性

  • 多种监控手段,语言探针和 service mesh
  • 多语言自动探针,Java,.NET Core 和 Node.JS
  • 轻量高效,不需要大数据
  • 模块化,UI、存储、集群管理多种机制可选
  • 支持告警
  • 优秀的可视化方案

SkyWalking 核心概念

  • Service - 服务。代表一组为传入请求提供相同的行为的工作负载。 使用代理或 SDK 时,可以定义服务名称。
  • Service Instance - 服务实例。服务组中的每个工作负载都称为一个实例。就像 Kubernetes 中的 Pod 一样,它在 OS 中不必是单个进程。
  • Endpoint - 端点。它是特定服务中用于传入请求的路径,例如 HTTP URI 路径或 RPC 服务类+方法签名。

二、SkyWalking 架构

从逻辑上讲,SkyWalking 分为四个部分:探针(Probes),平台后端,存储和 UI。

SkyWalking 架构

  • 探针(Probes) - 探针是指集成到目标系统中的代理或 SDK 库。它们负责收集数据(包括跟踪数据和统计数据)并将其按照 SkyWalking 的要求重新格式化为。
  • 平台后端 - 平台后端是一个提供后端服务的集群。它用于聚合、分析和驱动从探针到 UI 的流程。它还为传入格式(如 Zipkin 的格式),存储实现程序和集群管理提供可插入功能。 您甚至可以使用 Observability Analysis Language 自定义聚合和分析。
  • 存储 - 您可以选择一个 SkyWalking 已实现的存储,如由 Sharding-Sphere 管理的 ElasticSearch,H2 或 MySQL 集群,也可以自行实现一个存储。
  • UI - 用户界面很酷,对于 SkyWalking 最终用户而言非常强大。它也可以自定义以匹配您的自定义后端。

三、SkyWalking 安装

进入 Apache SkyWalking 官方下载页面,选择安装版本,下载解压到本地。

SkyWalking 组件

安装分为三个部分:

参考资料

Arthas 快速入门

Arthas 是 Alibaba 开源的 Java 诊断工具 。

简介

Arthas可以解决的问题:

  1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  5. 是否有一个全局视角来查看系统的运行状况?
  6. 有什么办法可以监控到 JVM 的实时运行状态?

Arthas支持 JDK 6+,支持 Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。

安装

使用arthas-boot(推荐)

下载arthas-boot.jar,然后用java -jar的方式启动:

1
2
wget https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar

打印帮助信息:

1
java -jar arthas-boot.jar -h
  • 如果下载速度比较慢,可以使用 aliyun 的镜像:

    1
    java -jar arthas-boot.jar --repo-mirror aliyun --use-http
  • 如果从 github 下载有问题,可以使用 gitee 镜像

    1
    wget https://arthas.gitee.io/arthas-boot.jar

使用as.sh

Arthas 支持在 Linux/Unix/Mac 等平台上一键安装,请复制以下内容,并粘贴到命令行中,敲 回车 执行即可:

1
curl -L https://alibaba.github.io/arthas/install.sh | sh

上述命令会下载启动脚本文件 as.sh 到当前目录,你可以放在任何地方或将其加入到 $PATH 中。

直接在 shell 下面执行./as.sh,就会进入交互界面。

也可以执行./as.sh -h来获取更多参数信息。

  • 如果从 github 下载有问题,可以使用 gitee 镜像

    1
    curl -L https://arthas.gitee.io/install.sh | sh

全量安装

最新版本,点击下载:下载地址

解压后,在文件夹里有arthas-boot.jar,直接用java -jar的方式启动:

1
java -jar arthas-boot.jar

打印帮助信息:

1
java -jar arthas-boot.jar -h

基础使用

启动 Demo

1
2
wget https://alibaba.github.io/arthas/arthas-demo.jar
java -jar arthas-demo.jar

arthas-demo是一个简单的程序,每隔一秒生成一个随机数,再执行质因式分解,并打印出分解结果。

arthas-demo源代码:查看

启动 arthas

在命令行下面执行(使用和目标进程一致的用户启动,否则可能 attach 失败):

1
2
wget https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar
  • 执行该程序的用户需要和目标进程具有相同的权限。比如以admin用户来执行:sudo su admin && java -jar arthas-boot.jarsudo -u admin -EH java -jar arthas-boot.jar
  • 如果 attach 不上目标进程,可以查看~/logs/arthas/ 目录下的日志。
  • 如果下载速度比较慢,可以使用 aliyun 的镜像:java -jar arthas-boot.jar --repo-mirror aliyun --use-http
  • java -jar arthas-boot.jar -h 打印更多参数信息。

选择应用 java 进程:

1
2
3
$ $ java -jar arthas-boot.jar
* [1]: 35542
[2]: 71560 arthas-demo.jar

Demo 进程是第 2 个,则输入 2,再输入回车/enter。Arthas 会 attach 到目标进程上,并输出日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[INFO] Try to attach process 71560
[INFO] Attach process 71560 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'

wiki: https://alibaba.github.io/arthas
version: 3.0.5.20181127201536
pid: 71560
time: 2018-11-28 19:16:24

$

查看 dashboard

输入dashboard,按回车/enter,会展示当前进程的信息,按ctrl+c可以中断执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ dashboard
ID NAME GROUP PRIORI STATE %CPU TIME INTERRU DAEMON
17 pool-2-thread-1 system 5 WAITIN 67 0:0 false false
27 Timer-for-arthas-dashb system 10 RUNNAB 32 0:0 false true
11 AsyncAppender-Worker-a system 9 WAITIN 0 0:0 false true
9 Attach Listener system 9 RUNNAB 0 0:0 false true
3 Finalizer system 8 WAITIN 0 0:0 false true
2 Reference Handler system 10 WAITIN 0 0:0 false true
4 Signal Dispatcher system 9 RUNNAB 0 0:0 false true
26 as-command-execute-dae system 10 TIMED_ 0 0:0 false true
13 job-timeout system 9 TIMED_ 0 0:0 false true
1 main main 5 TIMED_ 0 0:0 false false
14 nioEventLoopGroup-2-1 system 10 RUNNAB 0 0:0 false false
18 nioEventLoopGroup-2-2 system 10 RUNNAB 0 0:0 false false
23 nioEventLoopGroup-2-3 system 10 RUNNAB 0 0:0 false false
15 nioEventLoopGroup-3-1 system 10 RUNNAB 0 0:0 false false
Memory used total max usage GC
heap 32M 155M 1820M 1.77% gc.ps_scavenge.count 4
ps_eden_space 14M 65M 672M 2.21% gc.ps_scavenge.time(m 166
ps_survivor_space 4M 5M 5M s)
ps_old_gen 12M 85M 1365M 0.91% gc.ps_marksweep.count 0
nonheap 20M 23M -1 gc.ps_marksweep.time( 0
code_cache 3M 5M 240M 1.32% ms)
Runtime
os.name Mac OS X
os.version 10.13.4
java.version 1.8.0_162
java.home /Library/Java/JavaVir
tualMachines/jdk1.8.0
_162.jdk/Contents/Hom
e/jre

通过 thread 命令来获取到arthas-demo进程的 Main Class

thread 1会打印线程 ID 1 的栈,通常是 main 函数的线程。

1
2
$ thread 1 | grep 'main('
at demo.MathGame.main(MathGame.java:17)

通过 jad 来反编译 Main Class

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
$ jad demo.MathGame

ClassLoader:
+-sun.misc.Launcher$AppClassLoader@3d4eac69
+-sun.misc.Launcher$ExtClassLoader@66350f69

Location:
/tmp/arthas-demo.jar

/*
* Decompiled with CFR 0_132.
*/
package demo;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class MathGame {
private static Random random = new Random();
private int illegalArgumentCount = 0;

public static void main(String[] args) throws InterruptedException {
MathGame game = new MathGame();
do {
game.run();
TimeUnit.SECONDS.sleep(1L);
} while (true);
}

public void run() throws InterruptedException {
try {
int number = random.nextInt();
List<Integer> primeFactors = this.primeFactors(number);
MathGame.print(number, primeFactors);
}
catch (Exception e) {
System.out.println(String.format("illegalArgumentCount:%3d, ", this.illegalArgumentCount) + e.getMessage());
}
}

public static void print(int number, List<Integer> primeFactors) {
StringBuffer sb = new StringBuffer("" + number + "=");
Iterator<Integer> iterator = primeFactors.iterator();
while (iterator.hasNext()) {
int factor = iterator.next();
sb.append(factor).append('*');
}
if (sb.charAt(sb.length() - 1) == '*') {
sb.deleteCharAt(sb.length() - 1);
}
System.out.println(sb);
}

public List<Integer> primeFactors(int number) {
if (number < 2) {
++this.illegalArgumentCount;
throw new IllegalArgumentException("number is: " + number + ", need >= 2");
}
ArrayList<Integer> result = new ArrayList<Integer>();
int i = 2;
while (i <= number) {
if (number % i == 0) {
result.add(i);
number /= i;
i = 2;
continue;
}
++i;
}
return result;
}
}

Affect(row-cnt:1) cost in 970 ms.

watch

通过watch命令来查看demo.MathGame#primeFactors函数的返回值:

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
$ watch demo.MathGame primeFactors returnObj
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 107 ms.
ts=2018-11-28 19:22:30; [cost=1.715367ms] result=null
ts=2018-11-28 19:22:31; [cost=0.185203ms] result=null
ts=2018-11-28 19:22:32; [cost=19.012416ms] result=@ArrayList[
@Integer[5],
@Integer[47],
@Integer[2675531],
]
ts=2018-11-28 19:22:33; [cost=0.311395ms] result=@ArrayList[
@Integer[2],
@Integer[5],
@Integer[317],
@Integer[503],
@Integer[887],
]
ts=2018-11-28 19:22:34; [cost=10.136007ms] result=@ArrayList[
@Integer[2],
@Integer[2],
@Integer[3],
@Integer[3],
@Integer[31],
@Integer[717593],
]
ts=2018-11-28 19:22:35; [cost=29.969732ms] result=@ArrayList[
@Integer[5],
@Integer[29],
@Integer[7651739],
]

更多的功能可以查看进阶使用

退出 arthas

如果只是退出当前的连接,可以用quit或者exit命令。Attach 到目标进程上的 arthas 还会继续运行,端口会保持开放,下次连接时可以直接连接上。

如果想完全退出 arthas,可以执行shutdown命令。

进阶使用

基础命令

  • help——查看命令帮助信息
  • cat——打印文件内容,和 linux 里的 cat 命令类似
  • pwd——返回当前的工作目录,和 linux 命令类似
  • cls——清空当前屏幕区域
  • session——查看当前会话的信息
  • reset——重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类
  • version——输出当前目标 Java 进程所加载的 Arthas 版本号
  • history——打印命令历史
  • quit——退出当前 Arthas 客户端,其他 Arthas 客户端不受影响
  • shutdown——关闭 Arthas 服务端,所有 Arthas 客户端全部退出
  • keymap——Arthas 快捷键列表及自定义快捷键

jvm 相关

  • dashboard——当前系统的实时数据面板
  • thread——查看当前 JVM 的线程堆栈信息
  • jvm——查看当前 JVM 的信息
  • sysprop——查看和修改 JVM 的系统属性
  • sysenv——查看 JVM 的环境变量
  • vmoption——查看和修改 JVM 里诊断相关的 option
  • logger——查看和修改 logger
  • getstatic——查看类的静态属性
  • ognl——执行 ognl 表达式
  • mbean——查看 Mbean 的信息
  • heapdump——dump java heap, 类似 jmap 命令的 heap dump 功能

class/classloader 相关

  • sc——查看 JVM 已加载的类信息
  • sm——查看已加载类的方法信息
  • jad——反编译指定已加载类的源码
  • mc——内存编绎器,内存编绎.java文件为.class文件
  • redefine——加载外部的.class文件,redefine 到 JVM 里
  • dump——dump 已加载类的 byte code 到特定目录
  • classloader——查看 classloader 的继承树,urls,类加载信息,使用 classloader 去 getResource

monitor/watch/trace 相关

请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 shutdown 或将增强过的类执行 reset 命令。

  • monitor——方法执行监控
  • watch——方法执行数据观测
  • trace——方法内部调用路径,并输出方法路径上的每个节点上耗时
  • stack——输出当前方法被调用的调用路径
  • tt——方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测

options

  • options——查看或设置 Arthas 全局开关

管道

Arthas 支持使用管道对上述命令的结果进行进一步的处理,如sm java.lang.String * | grep 'index'

  • grep——搜索满足条件的  结果
  • plaintext——将  命令的结果去除 ANSI 颜色
  • wc——按行统计输出结果

后台异步任务

当线上出现偶发的问题,比如需要 watch 某个条件,而这个条件一天可能才会出现一次时,异步后台任务就派上用场了,详情请参考这里

  • 使用 > 将结果重写向到日志文件,使用 & 指定命令是后台运行,session 断开不影响任务执行(生命周期默认为 1 天)
  • jobs——列出所有 job
  • kill——强制终止任务
  • fg——将暂停的任务拉到前台执行
  • bg——将暂停的任务放到后台执行

Web Console

通过 websocket 连接 Arthas。

用户数据回报

3.1.4版本后,增加了用户数据回报功能,方便统一做安全或者历史数据统计。

在启动时,指定stat-url,就会回报执行的每一行命令,比如: ./as.sh --stat-url 'http://192.168.10.11:8080/api/stat'

在 tunnel server 里有一个示例的回报代码,用户可以自己在服务器上实现。

StatController.java

其他特性

参考资料

Maven 快速入门

Maven 简介

Maven 是什么

Maven 是一个项目管理工具。它负责管理项目开发过程中的几乎所有的东西。

  • 版本 - maven 有自己的版本定义和规则。
  • 构建 - maven 支持许多种的应用程序类型,对于每一种支持的应用程序类型都定义好了一组构建规则和工具集。
  • 输出物管理 - maven 可以管理项目构建的产物,并将其加入到用户库中。这个功能可以用于项目组和其他部门之间的交付行为。
  • 依赖关系 - maven 对依赖关系的特性进行细致的分析和划分,避免开发过程中的依赖混乱和相互污染行为
  • 文档和构建结果 - maven 的 site 命令支持各种文档信息的发布,包括构建过程的各种输出,javadoc,产品文档等。
  • 项目关系 - 一个大型的项目通常有几个小项目或者模块组成,用 maven 可以很方便地管理。
  • 移植性管理 - maven 可以针对不同的开发场景,输出不同种类的输出结果。

Maven 的生命周期

maven 把项目的构建划分为不同的生命周期(lifecycle)。粗略一点的话,它这个过程(phase)包括:编译、测试、打包、集成测试、验证、部署。maven 中所有的执行动作(goal)都需要指明自己在这个过程中的执行位置,然后 maven 执行的时候,就依照过程的发展依次调用这些 goal 进行各种处理。

这个也是 maven 的一个基本调度机制。一般来说,位置稍后的过程都会依赖于之前的过程。当然,maven 同样提供了配置文件,可以依照用户要求,跳过某些阶段。

Maven 的标准工程结构

Maven 的标准工程结构如下:

1
2
3
4
5
6
7
8
9
|-- pom.xml(maven的核心配置文件)
|-- src
|-- main
|-- java(java源代码目录)
|-- resources(资源文件目录)
|-- test
|-- java(单元测试代码目录)
|-- target(输出目录,所有的输出物都存放在这个目录下)
|-- classes(编译后的class文件存放处)

Maven 的”约定优于配置”

所谓的”约定优于配置”,在 maven 中并不是完全不可以修改的,他们只是一些配置的默认值而已。但是除非必要,并不需要去修改那些约定内容。maven 默认的文件存放结构如下:

每一个阶段的任务都知道怎么正确完成自己的工作,比如 compile 任务就知道从 src/main/java 下编译所有的 java 文件,并把它的输出 class 文件存放到 target/classes 中。

对 maven 来说,采用”约定优于配置”的策略可以减少修改配置的工作量,也可以降低学习成本,更重要的是,给项目引入了统一的规范。

Maven 的版本规范

maven 使用如下几个要素来唯一定位某一个输出物:

  • groupId - 团体、组织的标识符。团体标识的约定是,它以创建这个项目的组织名称的逆向域名(reverse domain name)开头。一般对应着 JAVA 的包的结构。例如 org.apache
  • artifactId - 单独项目的唯一标识符。比如我们的 tomcat, commons 等。不要在 artifactId 中包含点号(.)。
  • version - 一个项目的特定版本。
  • packaging - 项目的类型,默认是 jar,描述了项目打包后的输出。类型为 jar 的项目产生一个 JAR 文件,类型为 war 的项目产生一个 web 应用。

例如:想在 maven 工程中引入 4.12 版本的 junit 包,添加如下依赖即可。

1
2
3
4
5
6
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>compile</scope>
</dependency>

maven 有自己的版本规范,一般是如下定义 <major version><minor version><incremental version>-<qualifier> ,比如 1.2.3-beta-01。要说明的是,maven 自己判断版本的算法是 major,minor,incremental 部分用数字比 较,qualifier 部分用字符串比较,所以要小心 alpha-2 和 alpha-15 的比较关系,最好用 alpha-02 的格式。

maven 在版本管理时候可以使用几个特殊的字符串 SNAPSHOT,LATEST,RELEASE。比如”1.0-SNAPSHOT”。各个部分的含义和处理逻辑如下说明:

  • SNAPSHOT - 这个版本一般用于开发过程中,表示不稳定的版本。
  • LATEST - 指某个特定构件的最新发布,这个发布可能是一个发布版,也可能是一个 snapshot 版,具体看哪个时间最后。
  • RELEASE - 指最后一个发布版。

Maven 安装

Linux 环境安装可以使用我写一键安装脚本:https://github.com/dunwu/linux-tutorial/tree/master/codes/linux/ops/service/maven

环境准备

Maven 依赖于 Java,所以本地必须安装 JDK。

打开控制台,执行 java -version 确认本地已安装 JDK。

1
2
3
4
$ java -version
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)

下载解压

进入 **官网下载地址**,选择合适版本,下载并解压到本地。解压命令如下:

1
2
3
# 以下解压命令分别针对 zip 包和 tar 包
unzip apache-maven-3.6.3-bin.zip
tar xzvf apache-maven-3.6.3-bin.tar.gz

环境变量

添加环境变量 MAVEN_HOME,值为 Maven 的安装路径。

配置 Unix 系统环境变量

输入 vi /etc/profile ,添加环境变量如下:

1
2
3
# MAVEN 的根路径
export MAVEN_HOME=/opt/maven/apache-maven-3.5.2
export PATH=$MAVEN_HOME/bin:$PATH

执行 source /etc/profile ,立即生效。

配置 Windows 系统环境变量

右键 “计算机”,选择 “属性”,之后点击 “高级系统设置”,点击”环境变量”,来设置环境变量,有以下系统变量需要配置:

img

img

检测安装成功

检验是否安装成功,执行 mvn -v 命令,如果输出类似下面的 maven 版本信息,说明配置成功。

1
2
3
4
5
6
$ mvn -v
Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-18T02:33:14+08:00)
Maven home: /opt/maven/apache-maven-3.5.4
Java version: 1.8.0_191, vendor: Oracle Corporation, runtime: /mnt/disk1/jdk1.8.0_191/jre
Default locale: zh_CN, platform encoding: UTF-8
OS name: "linux", version: "3.10.0-327.el7.x86_64", arch: "amd64", family: "unix"

Maven 配置文件

setting.xml 文件是 Maven 的默认配置文件,其默认路径为:<Maven 安装目录>/conf/settings.xml

如果需要修改 Maven 配置,直接修改 setting.xml 并保持即可。

例如:想要修改本地仓库位置可以按如下配置,这样,所有通过 Maven 下载打包的 jar 包都会存储在 D:\maven\repo 路径下。

1
2
3
4
5
<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
<localRepository>D:\maven\repo<localRepository/>
<!-- 略 -->
</settings>

快速入门

创建 Maven 工程

初始化工程

执行指令:

1
mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=my-app -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false

会在当前路径新建一个名为 my-app 的 Maven 工程,其目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
my-app
|-- pom.xml
`-- src
|-- main
| `-- java
| `-- com
| `-- mycompany
| `-- app
| `-- App.java
`-- test
`-- java
`-- com
`-- mycompany
`-- app
`-- AppTest.java

其中, src/main/java 目录包含 java 源码, src/test/java 目录包含 java 测试源码,而 pom.xml 文件是 maven 工程的配置文件。

POM 配置

pom.xml 是 maven 工程的配置文件,它描述了 maven 工程的构建方式,其配置信息是很复杂的,这里给一个最简单的配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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>

<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

构建项目

执行以下命令,即可构建项目:

1
mvn clean package -Dmaven.test.skip=true -B -U

构建成功后,会输出类似下面的信息:

1
2
3
4
5
6
7
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.953 s
[INFO] Finished at: 2019-11-24T13:05:10+01:00
[INFO] ------------------------------------------------------------------------

这时,在当前路径下会产生一个 target 目录,其中包含了构建的输出物,如:jar 包、class 文件等。

这时,我们可以执行以下命令启动 jar 包:

1
java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App

在 Intellij 中创建 Maven 工程

(1)创建 Maven 工程

依次点击 File -> New -> Project 打开创建工程对话框,选择 Maven 工程。

img

(2)输入项目信息

img

(3)点击 Intellij 侧边栏中的 Maven 工具界面,有几个可以直接使用的 maven 命令,可以帮助你进行构建。

img

在 Eclipse 中创建 Maven 工程

(1)Maven 插件

在 Eclipse 中创建 Maven 工程,需要安装 Maven 插件。

一般较新版本的 Eclipse 都会带有 Maven 插件,如果你的 Eclipse 中已经有 Maven 插件,可以跳过这一步骤。

点击 Help -> Eclipse Marketplace,搜索 maven 关键字,选择安装红框对应的 Maven 插件。

img

(2)Maven 环境配置

点击 Window -> Preferences

如下图所示,配置 settings.xml 文件的位置

img

(3)创建 Maven 工程

File -> New -> Maven Project -> Next,在接下来的窗口中会看到一大堆的项目模板,选择合适的模板。

接下来设置项目的参数,如下:

img

groupId是项目组织唯一的标识符,实际对应 JAVA 的包的结构,是 main 目录里 java 的目录结构。

artifactId就是项目的唯一的标识符,实际对应项目的名称,就是项目根目录的名称。

点击 Finish,Eclipse 会创建一个 Maven 工程。

(4)使用 Maven 进行构建

Eclipse 中构建方式:

在 Elipse 项目上右击 -> Run As 就能看到很多 Maven 操作。这些操作和 maven 命令是等效的。例如 Maven clean,等同于 mvn clean 命令。

img

你也可以点击 Maven build,输入组合命令,并保存下来。如下图:

img

Maven 命令构建方式:

当然,你也可以直接使用 maven 命令进行构建。

进入工程所在目录,输入 maven 命令就可以了。

img

使用说明

如何添加依赖

在 Maven 工程中添加依赖 jar 包,很简单,只要在 POM 文件中引入对应的<dependency>标签即可。

参考下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<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>
<groupId>com.zp.maven</groupId>
<artifactId>MavenDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>MavenDemo</name>
<url>http://maven.apache.org</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>3.8.1</junit.version>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

<dependency> 标签最常用的四个属性标签:

  • <groupId> - 项目组织唯一的标识符,实际对应 JAVA 的包的结构。
  • <artifactId> - 项目唯一的标识符,实际对应项目的名称,就是项目根目录的名称。
  • <version> - jar 包的版本号。可以直接填版本数字,也可以在 properties 标签中设置属性值。
  • <scope> - jar 包的作用范围。可以填写 compile、runtime、test、system 和 provided。用来在编译、测试等场景下选择对应的 classpath。

如何寻找 jar 包

可以在 http://mvnrepository.com/ 站点搜寻你想要的 jar 包版本

例如,想要使用 log4j,可以找到需要的版本号,然后拷贝对应的 maven 标签信息,将其添加到 pom .xml 文件中。

如何使用 Maven 插件(Plugin)

要添加 Maven 插件,可以在 pom.xml 文件中添加 <plugin> 标签。

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

<configuration> 标签用来配置插件的一些使用参数。

如何一次编译多个工程

假设要创建一个父 maven 工程,它有两个子工程:my-app 和 my-webapp:

1
2
3
4
5
6
7
8
9
10
11
+- pom.xml
+- my-app
| +- pom.xml
| +- src
| +- main
| +- java
+- my-webapp
| +- pom.xml
| +- src
| +- main
| +- webapp

app 工程的 pom.xml 如下,重点在于在 modules 中引入两个子 module:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>

<groupId>com.mycompany.app</groupId>
<artifactId>app</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<modules>
<module>my-app</module>
<module>my-webapp</module>
</modules>
</project>

选择编译 XXX 时,会依次对它的所有 Module 执行相同操作。

常用 Maven 插件

更多详情请参考:https://maven.apache.org/plugins/

maven-antrun-plugin

maven-antrun-plugin 能让用户在 Maven 项目中运行 Ant 任务。用户可以直接在该插件的配置以 Ant 的方式编写 Target, 然后交给该插件的 run 目标去执行。在一些由 Ant 往 Maven 迁移的项目中,该插件尤其有用。此外当你发现需要编写一些自定义程度很高的任务,同时又觉 得 Maven 不够灵活时,也可以以 Ant 的方式实现之。maven-antrun-plugin 的 run 目标通常与生命周期绑定运行。

maven-archetype-plugin

Archtype 指项目的骨架,Maven 初学者最开始执行的 Maven 命令可能就是mvn archetype:generate,这实际上就是让 maven-archetype-plugin 生成一个很简单的项目骨架,帮助开发者快速上手。可能也有人看到一些文档写了mvn archetype:create, 但实际上 create 目标已经被弃用了,取而代之的是 generate 目标,该目标使用交互式的方式提示用户输入必要的信息以创建项目,体验更好。 maven-archetype-plugin 还有一些其他目标帮助用户自己定义项目原型,例如你由一个产品需要交付给很多客户进行二次开发,你就可以为 他们提供一个 Archtype,帮助他们快速上手。

maven-assembly-plugin

maven-assembly-plugin 的用途是将项目打包,该包可能包含了项目的可执行文件、源代码、readme、平台脚本等等。 maven-assembly-plugin 支持各种主流的格式如 zip、tar.gz、jar 和 war 等,具体打包哪些文件是高度可控的,例如用户可以 按文件级别的粒度、文件集级别的粒度、模块级别的粒度、以及依赖级别的粒度控制打包,此外,包含和排除配置也是支持的。maven-assembly- plugin 要求用户使用一个名为assembly.xml的元数据文件来表述打包,它的 single 目标可以直接在命令行调用,也可以被绑定至生命周期。

maven-dependency-plugin

maven-dependency-plugin 最大的用途是帮助分析项目依赖,dependency:list能够列出项目最终解析到的依赖列表,dependency:tree能进一步的描绘项目依赖树,dependency:analyze可以告诉你项目依赖潜在的问题,如果你有直接使用到的却未声明的依赖,该目标就会发出警告。maven-dependency-plugin 还有很多目标帮助你操作依赖文件,例如dependency:copy-dependencies能将项目依赖从本地 Maven 仓库复制到某个特定的文件夹下面。

maven-enforcer-plugin

在一个稍大一点的组织或团队中,你无法保证所有成员都熟悉 Maven,那他们做一些比较愚蠢的事情就会变得很正常,例如给项目引入了外部的 SNAPSHOT 依赖而导致构建不稳定,使用了一个与大家不一致的 Maven 版本而经常抱怨构建出现诡异问题。maven-enforcer- plugin 能够帮助你避免之类问题,它允许你创建一系列规则强制大家遵守,包括设定 Java 版本、设定 Maven 版本、禁止某些依赖、禁止 SNAPSHOT 依赖。只要在一个父 POM 配置规则,然后让大家继承,当规则遭到破坏的时候,Maven 就会报错。除了标准的规则之外,你还可以扩展该插 件,编写自己的规则。maven-enforcer-plugin 的 enforce 目标负责检查规则,它默认绑定到生命周期的 validate 阶段。

maven-help-plugin

maven-help-plugin 是一个小巧的辅助工具,最简单的help:system可以打印所有可用的环境变量和 Java 系统属性。help:effective-pomhelp:effective-settings最 为有用,它们分别打印项目的有效 POM 和有效 settings,有效 POM 是指合并了所有父 POM(包括 Super POM)后的 XML,当你不确定 POM 的某些信息从何而来时,就可以查看有效 POM。有效 settings 同理,特别是当你发现自己配置的 settings.xml 没有生效时,就可以用help:effective-settings来验证。此外,maven-help-plugin 的 describe 目标可以帮助你描述任何一个 Maven 插件的信息,还有 all-profiles 目标和 active-profiles 目标帮助查看项目的 Profile。

maven-release-plugin

maven-release-plugin 的用途是帮助自动化项目版本发布,它依赖于 POM 中的 SCM 信息。release:prepare用来准备版本发布,具体的工作包括检查是否有未提交代码、检查是否有 SNAPSHOT 依赖、升级项目的 SNAPSHOT 版本至 RELEASE 版本、为项目打标签等等。release:perform则 是签出标签中的 RELEASE 源码,构建并发布。版本发布是非常琐碎的工作,它涉及了各种检查,而且由于该工作仅仅是偶尔需要,因此手动操作很容易遗漏一 些细节,maven-release-plugin 让该工作变得非常快速简便,不易出错。maven-release-plugin 的各种目标通常直接在 命令行调用,因为版本发布显然不是日常构建生命周期的一部分。

maven-resources-plugin

为了使项目结构更为清晰,Maven 区别对待 Java 代码文件和资源文件,maven-compiler-plugin 用来编译 Java 代码,maven-resources-plugin 则用来处理资源文件。默认的主资源文件目录是src/main/resources,很多用户会需要添加额外的资源文件目录,这个时候就可以通过配置 maven-resources-plugin 来实现。此外,资源文件过滤也是 Maven 的一大特性,你可以在资源文件中使用*${propertyName}*形式的 Maven 属性,然后配置 maven-resources-plugin 开启对资源文件的过滤,之后就可以针对不同环境通过命令行或者 Profile 传入属性的值,以实现更为灵活的构建。

maven-surefire-plugin

可能是由于历史的原因,Maven 2.3 中用于执行测试的插件不是 maven-test-plugin,而是 maven-surefire-plugin。其实大部分时间内,只要你的测试 类遵循通用的命令约定(以 Test 结尾、以 TestCase 结尾、或者以 Test 开头),就几乎不用知晓该插件的存在。然而在当你想要跳过测试、排除某些 测试类、或者使用一些 TestNG 特性的时候,了解 maven-surefire-plugin 的一些配置选项就很有用了。例如 mvn test -Dtest=FooTest 这样一条命令的效果是仅运行 FooTest 测试类,这是通过控制 maven-surefire-plugin 的 test 参数实现的。

build-helper-maven-plugin

Maven 默认只允许指定一个主 Java 代码目录和一个测试 Java 代码目录,虽然这其实是个应当尽量遵守的约定,但偶尔你还是会希望能够指定多个 源码目录(例如为了应对遗留项目),build-helper-maven-plugin 的 add-source 目标就是服务于这个目的,通常它被绑定到 默认生命周期的 generate-sources 阶段以添加额外的源码目录。需要强调的是,这种做法还是不推荐的,因为它破坏了 Maven 的约定,而且可能会遇到其他严格遵守约定的插件工具无法正确识别额外的源码目录。

build-helper-maven-plugin 的另一个非常有用的目标是 attach-artifact,使用该目标你可以以 classifier 的形式选取部分项目文件生成附属构件,并同时 install 到本地仓库,也可以 deploy 到远程仓库。

exec-maven-plugin

exec-maven-plugin 很好理解,顾名思义,它能让你运行任何本地的系统程序,在某些特定情况下,运行一个 Maven 外部的程序可能就是最简单的问题解决方案,这就是exec:exec的 用途,当然,该插件还允许你配置相关的程序运行参数。除了 exec 目标之外,exec-maven-plugin 还提供了一个 java 目标,该目标要求你 提供一个 mainClass 参数,然后它能够利用当前项目的依赖作为 classpath,在同一个 JVM 中运行该 mainClass。有时候,为了简单的 演示一个命令行 Java 程序,你可以在 POM 中配置好 exec-maven-plugin 的相关运行参数,然后直接在命令运行mvn exec:java 以查看运行效果。

jetty-maven-plugin

在进行 Web 开发的时候,打开浏览器对应用进行手动的测试几乎是无法避免的,这种测试方法通常就是将项目打包成 war 文件,然后部署到 Web 容器 中,再启动容器进行验证,这显然十分耗时。为了帮助开发者节省时间,jetty-maven-plugin 应运而生,它完全兼容 Maven 项目的目录结构,能够周期性地检查源文件,一旦发现变更后自动更新到内置的 Jetty Web 容器中。做一些基本配置后(例如 Web 应用的 contextPath 和自动扫描变更的时间间隔),你只要执行 mvn jetty:run ,然后在 IDE 中修改代码,代码经 IDE 自动编译后产生变更,再由 jetty-maven-plugin 侦测到后更新至 Jetty 容器,这时你就可以直接 测试 Web 页面了。需要注意的是,jetty-maven-plugin 并不是宿主于 Apache 或 Codehaus 的官方插件,因此使用的时候需要额外 的配置settings.xml的 pluginGroups 元素,将 org.mortbay.jetty 这个 pluginGroup 加入。

versions-maven-plugin

很多 Maven 用户遇到过这样一个问题,当项目包含大量模块的时候,为他们集体更新版本就变成一件烦人的事情,到底有没有自动化工具能帮助完成这件 事情呢?(当然你可以使用 sed 之类的文本操作工具,不过不在本文讨论范围)答案是肯定的,versions-maven- plugin 提供了很多目标帮助你管理 Maven 项目的各种版本信息。例如最常用的,命令 mvn versions:set -DnewVersion=1.1-SNAPSHOT 就能帮助你把所有模块的版本更新到 1.1-SNAPSHOT。该插件还提供了其他一些很有用的目标,display-dependency- updates 能告诉你项目依赖有哪些可用的更新;类似的 display-plugin-updates 能告诉你可用的插件更新;然后 use- latest-versions 能自动帮你将所有依赖升级到最新版本。最后,如果你对所做的更改满意,则可以使用 mvn versions:commit 提交,不满意的话也可以使用 mvn versions:revert 进行撤销。

Maven 命令

常用 maven 命令清单:

生命周期 阶段描述
mvn validate 验证项目是否正确,以及所有为了完整构建必要的信息是否可用
mvn generate-sources 生成所有需要包含在编译过程中的源代码
mvn process-sources 处理源代码,比如过滤一些值
mvn generate-resources 生成所有需要包含在打包过程中的资源文件
mvn process-resources 复制并处理资源文件至目标目录,准备打包
mvn compile 编译项目的源代码
mvn process-classes 后处理编译生成的文件,例如对 Java 类进行字节码增强(bytecode enhancement)
mvn generate-test-sources 生成所有包含在测试编译过程中的测试源码
mvn process-test-sources 处理测试源码,比如过滤一些值
mvn generate-test-resources 生成测试需要的资源文件
mvn process-test-resources 复制并处理测试资源文件至测试目标目录
mvn test-compile 编译测试源码至测试目标目录
mvn test 使用合适的单元测试框架运行测试。这些测试应该不需要代码被打包或发布
mvn prepare-package 在真正的打包之前,执行一些准备打包必要的操作。这通常会产生一个包的展开的处理过的版本(将会在 Maven 2.1+中实现)
mvn package 将编译好的代码打包成可分发的格式,如 JAR,WAR,或者 EAR
mvn pre-integration-test 执行一些在集成测试运行之前需要的动作。如建立集成测试需要的环境
mvn integration-test 如果有必要的话,处理包并发布至集成测试可以运行的环境
mvn post-integration-test 执行一些在集成测试运行之后需要的动作。如清理集成测试环境。
mvn verify 执行所有检查,验证包是有效的,符合质量规范
mvn install 安装包至本地仓库,以备本地的其它项目作为依赖使用
mvn deploy 复制最终的包至远程仓库,共享给其它开发人员和项目(通常和一次正式的发布相关)

示例:最常用的 maven 构建命令

1
mvn clean install -Dmaven.test.skip=true -B -U

清理本地输出物,并构建 maven 项目,最后将输出物归档在本地仓库。

:bulb: 想了解更多 maven 命令行细节可以参考官方文档:

参考资料

网络技术之 VPN

img

简介

虚拟专用网络(VPN)的功能是:在公用网络上建立专用网络,进行加密通讯。在企业网络中有广泛应用。VPN 网关通过对数据包的加密和数据包目标地址的转换实现远程访问。VPN 可通过服务器、硬件、软件等多种方式实现。

VPN 属于远程访问技术,简单地说就是利用公用网络架设专用网络。例如某公司员工出差到外地,他想访问企业内网的服务器资源,这种访问就属于远程访问。
在传统的企业网络配置中,要进行远程访问,传统的方法是租用 DDN(数字数据网)专线或帧中继,这样的通讯方案必然导致高昂的网络通讯和维护费用。对于移动用户(移动办公人员)与远端个人用户而言,一般会通过拨号线路(Internet)进入企业的局域网,但这样必然带来安全上的隐患。
让外地员工访问到内网资源,利用 VPN 的解决方法就是在内网中架设一台 VPN 服务器。外地员工在当地连上互联网后,通过互联网连接 VPN 服务器,然后通过 VPN 服务器进入企业内网。为了保证数据安全,VPN 服务器和客户机之间的通讯数据都进行了加密处理。有了数据加密,就可以认为数据是在一条专用的数据链路上进行安全传输,就如同专门架设了一个专用网络一样,但实际上 VPN 使用的是互联网上的公用链路,因此 VPN 称为虚拟专用网络,其实质上就是利用加密技术在公网上封装出一个数据通讯隧道。有了 VPN 技术,用户无论是在外地出差还是在家中办公,只要能上互联网就能利用 VPN 访问内网资源,这就是 VPN 在企业中应用得如此广泛的原因。

VPN 的作用

隐藏 IP 和位置

img

VPN 可以隐藏使用者的 IP 地址和位置。

使用 VPN 的最常见原因之一是屏蔽您的真实 IP 地址。

您的 IP 地址是由 ISP 分配的唯一数字地址。 您在线上所做的所有事情都链接到您的 IP 地址,因此可以用来将您与在线活动进行匹配。 大多数网站记录其访问者的 IP 地址。

广告商还可以使用您的 IP 地址,根据您的身份和浏览历史为您提供有针对性的广告。

连接到 VPN 服务器时,您将使用该 VPN 服务器的 IP 地址。 您访问的任何网站都会看到 VPN 服务器的 IP 地址,而不是您自己的。

您将能够绕过 IP 地址阻止并浏览网站,而不会将您的活动作为一个个人追溯到您。

通信加密

img

使用 VPN 时,可以对信息进行加密,使得密码,电子邮件,照片,银行数据和其他敏感信息不会被拦截。

如果在公共场所使用公共 WiFi 连接网络时,敏感数据有被盗的风险。黑客可以利用开放和未加密的网络来窃取重要数据,例如您的密码,电子邮件,照片,银行数据和其他敏感信息。

VPN 可以加密信息,使黑客更难以拦截和窃取数据。

翻墙

img

轻松解除对 Facebook 和 Twitter,Skype,YouTube 和 Gmail 等网站和服务的阻止。 即使您被告知您所在的国家/地区不可用它,或者您所在的学校或办公室网络限制访问,也可以获取所需的东西。

某些服务(例如 Netflix 或 BBC iPlayer)会根据您访问的国家/地区限制访问内容。使用 VPN 可以绕过这些地理限制并解锁“隐藏”内容的唯一可靠方法。

避免被监听

img

使用 VPN 可以向政府、ISP、黑客隐藏通信信息。

您的 Internet 服务提供商(ISP)可以看到您访问的所有网站,并且几乎可以肯定会记录该信息。

在某些国家/地区,ISP 需要长时间收集和存储用户数据,并且政府能够访问,存储和搜索该信息。

在美国,英国,澳大利亚和欧洲大部分地区就是这种情况,仅举几例。

由于 VPN 会加密从设备到 VPN 服务器的互联网流量,因此您的 ISP 或任何其他第三方将无法监视您的在线活动。

要了解有关监视技术和全球大规模监视问题的更多信息,请访问 EFF 和 Privacy International。 您还可以在此处找到全球监视披露的更新列表。

工作原理

VPN 会在您的设备和私人服务器之间建立私人和加密的互联网连接。 这意味着您的数据无法被 ISP 或任何其他第三方读取或理解。 然后,私有服务器将您的流量发送到您要访问的网站或服务上。

img

VPN 的基本处理过程如下:

  1. 要保护主机发送明文信息到其他 VPN 设备。
  2. VPN 设备根据网络管理员设置的规则,确定是对数据进行加密还是直接传输。
  3. 对需要加密的数据,VPN 设备将其整个数据包(包括要传输的数据、源 IP 地址和目的 lP 地址)进行加密并附上数据签名,加上新的数据报头(包括目的地 VPN 设备需要的安全信息和一些初始化参数)重新封装。
  4. 将封装后的数据包通过隧道在公共网络上传输。
  5. 数据包到达目的 VPN 设备后,将其解封,核对数字签名无误后,对数据包解密。

VPN 协议

img

  • OpenVPN

  • IKEv2 / IPSec

  • SSTP

  • PPTP

  • Wireguard

VPN 服务

你可以选择付费 VPN 或自行搭建 VPN。

VPN 服务商:

开源 VPN:

参考资料

深入剖析共识性算法 Paxos

Paxos 是一种基于消息传递且具有容错性的共识性(consensus)算法

Paxos 算法解决了分布式一致性问题:在一个节点数为 2N+1 的分布式集群中,只要半数以上的节点(N + 1)还正常工作,整个系统仍可以正常工作。

Paxos 背景

Paxos 是 Leslie Lamport 于 1990 年提出的一种基于消息传递且具有高度容错特性的共识(consensus)算法

为描述 Paxos 算法,Lamport 虚拟了一个叫做 Paxos 的希腊城邦,这个岛按照议会民主制的政治模式制订法律,但是没有人愿意将自己的全部时间和精力放在这种事情上。所以无论是议员,议长或者传递纸条的服务员都不能承诺别人需要时一定会出现,也无法承诺批准决议或者传递消息的时间。

Paxos 算法解决的问题正是分布式共识性问题,即一个分布式系统中的各个节点如何就某个值(决议)达成一致。

Paxos 算法运行在允许宕机故障的异步系统中,不要求可靠的消息传递,可容忍消息丢失、延迟、乱序以及重复。它利用大多数 (Majority) 机制保证了一定的容错能力,即 N 个节点的系统最多允许 N / 2 - 1 个节点同时出现故障。

Paxos 算法包含 2 个部分:

  • Basic Paxos 算法:描述的多节点之间如何就某个值达成共识。
  • Multi Paxos 思想:描述的是执行多个 Basic Paxos 实例,就一系列值达成共识。

Basic Paxos 算法

角色

Paxos 将分布式系统中的节点分 Proposer、Acceptor、Learner 三种角色。

  • 提议者(Proposer):发出提案(Proposal),用于投票表决。Proposal 信息包括提案编号 (Proposal ID) 和提议的值 (Value)。在绝大多数场景中,集群中收到客户端请求的节点,才是提议者。这样做的好处是,对业务代码没有入侵性,也就是说,我们不需要在业务代码中实现算法逻辑。
  • 接受者(Acceptor):对每个 Proposal 进行投票,若 Proposal 获得多数 Acceptor 的接受,则称该 Proposal 被批准。一般来说,集群中的所有节点都在扮演接受者的角色,参与共识协商,并接受和存储数据。
  • 学习者(Learner):不参与接受,从 Proposers/Acceptors 学习、记录最新达成共识的提案(Value)。一般来说,学习者是数据备份节点,比如主从架构中的从节点,被动地接受数据,容灾备份。

在多副本状态机中,每个副本都同时具有 Proposer、Acceptor、Learner 三种角色。

这三种角色,在本质上代表的是三种功能:

  • 提议者代表的是接入和协调功能,收到客户端请求后,发起二阶段提交,进行共识协商;
  • 接受者代表投票协商和存储数据,对提议的值进行投票,并接受达成共识的值,存储保存;
  • 学习者代表存储数据,不参与共识协商,只接受达成共识的值,存储保存。

算法

Paxos 算法有 3 个阶段,其中,前 2 个阶段负责协商并达成共识:

  1. 准备(Prepare)阶段:Proposer 向 Acceptors 发出 Prepare 请求,Acceptors 针对收到的 Prepare 请求进行 Promise 承诺。
  2. 接受(Accept)阶段:Proposer 收到多数 Acceptors 承诺的 Promise 后,向 Acceptors 发出 Propose 请求,Acceptors 针对收到的 Propose 请求进行 Accept 处理。
  3. 学习(Learn)阶段:Proposer 在收到多数 Acceptors 的 Accept 之后,标志着本次 Accept 成功,决议形成,将形成的决议发送给所有 Learners。

看到这里,了解过分布式事务的读者,想必会觉得眼熟:这不就是两阶段提交嘛!没错,这里采用的正式两阶段提交的思想。两阶段提交是达成共识的常用方式。

Paxos 算法流程中的每条消息描述如下:

  • Prepare: Proposer 生成全局唯一且递增的 Proposal ID (可使用时间戳加 Server ID),向所有 Acceptors 发送 Prepare 请求,这里无需携带提案内容,只携带 Proposal ID 即可。
  • Promise: Acceptors 收到 Prepare 请求后,做出“两个承诺,一个应答”。
    • 两个承诺:
      • 不再接受 Proposal ID 小于等于当前请求的 Prepare 请求。
      • 不再接受 Proposal ID 小于当前请求的 Propose 请求。
    • 一个应答:
      • 不违背以前作出的承诺下,回复已经 Accept 过的提案中 Proposal ID 最大的那个提案的 Value 和 Proposal ID,没有则返回空值。
  • Propose: Proposer 收到多数 Acceptors 的 Promise 应答后,从应答中选择 Proposal ID 最大的提案的 Value,作为本次要发起的提案。如果所有应答的提案 Value 均为空值,则可以自己随意决定提案 Value。然后携带当前 Proposal ID,向所有 Acceptors 发送 Propose 请求。
  • Accept: Acceptor 收到 Propose 请求后,在不违背自己之前作出的承诺下,接受并持久化当前 Proposal ID 和提案 Value。
  • Learn: Proposer 收到多数 Acceptors 的 Accept 后,决议形成,将形成的决议发送给所有 Learners。

Prepare 阶段

在准备请求中是不需要指定提议的值的,只需要携带提案编号就可以了。

下图的示例中,首先客户端 1、2 作为提议者,分别向所有接受者发送包含提案编号的准备请求:

接着,当节点 A、B 收到提案编号为 1 的准备请求,节点 C 收到提案编号为 5 的准备请求后,将进行这样的处理:

  • 由于之前没有通过任何提案,所以节点 A、B 将返回一个 “尚无提案” 的响应。也就是说节点 A 和 B 在告诉提议者,我之前没有通过任何提案呢,并承诺以后不再响应提案编号小于等于 1 的准备请求,不会通过编号小于 1 的提案。
  • 节点 C 也是如此,它将返回一个 “尚无提案”的响应,并承诺以后不再响应提案编号小于等于 5 的准备请求,不会通过编号小于 5 的提案。

另外,当节点 A、B 收到提案编号为 5 的准备请求,和节点 C 收到提案编号为 1 的准备请求的时候,将进行这样的处理过程:

  • 当节点 A、B 收到提案编号为 5 的准备请求的时候,因为提案编号 5 大于它们之前响应的准备请求的提案编号 1,而且两个节点都没有通过任何提案,所以它将返回一个 “尚无提案”的响应,并承诺以后不再响应提案编号小于等于 5 的准备请求,不会通过编号小于 5 的提案。

  • 当节点 C 收到提案编号为 1 的准备请求的时候,由于提案编号 1 小于它之前响应的准备请求的提案编号 5,所以丢弃该准备请求,不做响应。

Accept 阶段

首先客户端 1、2 在收到大多数节点的准备响应之后,会分别发送接受请求:

  • 当客户端 1 收到大多数的接受者(节点 A、B)的准备响应后,根据响应中提案编号最大的提案的值,设置接受请求中的值。因为该值在来自节点 A、B 的准备响应中都为空(也就是图 5 中的“尚无提案”),所以就把自己的提议值 3 作为提案的值,发送接受请求[1, 3]。

  • 当客户端 2 收到大多数的接受者的准备响应后(节点 A、B 和节点 C),根据响应中提案编号最大的提案的值,来设置接受请求中的值。因为该值在来自节点 A、B、C 的准备响应中都为空(也就是图 5 和图 6 中的“尚无提案”),所以就把自己的提议值 7 作为提案的值,发送接受请求[5, 7]。

当三个节点收到 2 个客户端的接受请求时,会进行这样的处理:

  • 当节点 A、B、C 收到接受请求[1, 3]的时候,由于提案的提案编号 1 小于三个节点承诺能通过的提案的最小提案编号 5,所以提案[1, 3]将被拒绝。
  • 当节点 A、B、C 收到接受请求[5, 7]的时候,由于提案的提案编号 5 不小于三个节点承诺能通过的提案的最小提案编号 5,所以就通过提案[5, 7],也就是接受了值 7,三个节点就 X 值为 7 达成了共识。

小结

Basic Paxos 是通过二阶段提交的方式来达成共识的。

除了共识,Basic Paxos 还实现了容错,在少于一半的节点出现故障时,集群也能工作。它不像分布式事务算法那样,必须要所有节点都同意后才提交操作,因为“所有节点都同意”这个原则,在出现节点故障的时候会导致整个集群不可用。也就是说,“大多数节点都同意”的原则,赋予了 Basic Paxos 容错的能力,让它能够容忍少于一半的节点的故障。

本质上而言,提案编号的大小代表着优先级,你可以这么理解,根据提案编号的大小,接受者保证三个承诺,具体来说:

  • 如果准备请求的提案编号,小于等于接受者已经响应的准备请求的提案编号,那么接受者将承诺不响应这个准备请求;
  • 如果接受请求中的提案的提案编号,小于接受者已经响应的准备请求的提案编号,那么接受者将承诺不通过这个提案;
  • 如果接受者之前有通过提案,那么接受者将承诺,会在准备请求的响应中,包含已经通过的最大编号的提案信息。

Multi Paxos 思想

兰伯特提到的 Multi-Paxos 是一种思想,不是算法。而 Multi-Paxos 算法是一个统称,它是指基于 Multi-Paxos 思想,通过多个 Basic Paxos 实例实现一系列值的共识的算法(比如 Chubby 的 Multi-Paxos 实现、Raft 算法等)。

Basic Paxos 的问题

Basic Paxos 有以下问题,导致它不能应用于实际:

  • Basic Paxos 算法只能对一个值形成决议
  • Basic Paxos 算法会消耗大量网络带宽。Basic Paxos 中,决议的形成至少需要两次网络通信,在高并发情况下可能需要更多的网络通信,极端情况下甚至可能形成活锁。如果想连续确定多个值,Basic Paxos 搞不定了。

Multi Paxos 的改进

Multi Paxos 正是为解决以上问题而提出。Multi Paxos 基于 Basic Paxos 做了两点改进:

  • 针对每一个要确定的值,运行一次 Paxos 算法实例(Instance),形成决议。每一个 Paxos 实例使用唯一的 Instance ID 标识。
  • 在所有 Proposer 中选举一个 Leader,由 Leader 唯一地提交 Proposal 给 Acceptor 进行表决。这样没有 Proposer 竞争,解决了活锁问题。在系统中仅有一个 Leader 进行 Value 提交的情况下,Prepare 阶段就可以跳过,从而将两阶段变为一阶段,提高效率。

Multi Paxos 首先需要选举 Leader,Leader 的确定也是一次决议的形成,所以可执行一次 Basic Paxos 实例来选举出一个 Leader。选出 Leader 之后只能由 Leader 提交 Proposal,在 Leader 宕机之后服务临时不可用,需要重新选举 Leader 继续服务。在系统中仅有一个 Leader 进行 Proposal 提交的情况下,Prepare 阶段可以跳过。

Multi Paxos 通过改变 Prepare 阶段的作用范围至后面 Leader 提交的所有实例,从而使得 Leader 的连续提交只需要执行一次 Prepare 阶段,后续只需要执行 Accept 阶段,将两阶段变为一阶段,提高了效率。为了区分连续提交的多个实例,每个实例使用一个 Instance ID 标识,Instance ID 由 Leader 本地递增生成即可。

Multi Paxos 允许有多个自认为是 Leader 的节点并发提交 Proposal 而不影响其安全性,这样的场景即退化为 Basic Paxos。

Chubby 和 Boxwood 均使用 Multi Paxos。ZooKeeper 使用的 Zab 也是 Multi Paxos 的变形。

参考资料