本文介绍一个生僻但推荐使用的命令 rebase(衍合)。

使用场景

衍合的两个使用场景:

  1. 整理提交历史
  2. 生成干净补丁

整理提交历史

开发过程中,常常需要定期将最新的远程分支拉取(pull)到本地分支,保持本地代码最新(up to date)。如果拉取频繁,pull 默认的 merge 行为会造成祖先图谱(ancestry graph)无谓的复杂:

1
$ git pull origin master    // pull = fetch + merge

*   ab900eb - 三方合并版本(注意这里!)
|\
| * 756ba83 - 本地分支提交的版本
* | 915fe84 - 先被推送到远程分支的版本
|/
*   e7ce3f8 - 基准版本(共同祖先)

此时推荐使用 rebase 命令!其产生的祖先图谱如下,非常简洁:

1
*   dc6baf3 - 本地分支提交的版本(注意这个提交被改写了!)
|
*   915fe84 - 先被推送到远程分支的版本
|
*   e7ce3f8 - 基准版本(共同祖先)

可见,这个神奇的命令功能类似 merge ,但它避免了上述无谓的合并节点,从而产生一个更为整洁的提交历史。如果视察一个衍合过的分支历史,仿佛所有的提交都是在一根时间轴上先后进行的,尽管实际上它们原本是同时并行发生的。这么做的好处是,非常便于项目管理人员按时间轴进行代码审查

命令用法

可以在 pull 时主动加上 --rebase 参数:

1
$ git pull --rebase origin master

甚至推荐将 rebase 设为 pull 命令的默认行为,从而应用于所有新建分支:

1
$ git config --global branch.autosetuprebase always

注意,对于应用上述命令前已存在的分支(例如 master),需要补充执行如下配置:

1
$ git config branch.master.rebase true

命令原理

下面进一步介绍rebase 命令的原理:

  1. 把本地分支从上一次 pull 之后的变更暂存起来;
  2. 恢复到上一次 pull 时的情况;
  3. 合并远程分支的提交;
  4. 最后再逐一合并刚暂存下来的本地提交(相当于重放一遍)。

生成干净补丁

另一个使用衍合的目的,是想要得到一个能在远程分支上干净应用的补丁 — 比如某些项目你不是维护者,但想帮点忙的话,最好用衍合:先在自己的一个独立分支中进行开发,当准备向主项目提交补丁的时候,根据最新的 origin/master 进行一次衍合操作然后再提交,这样维护者就不需要做任何整合工作(实际上是把解决分支补丁同最新主干代码之间冲突的责任,化转为由提交补丁的人来解决。),只需根据你提供的仓库地址作一次快进合并,或者直接采纳你提交的补丁。

使用风险

注意,衍合必须遵守的准则:一旦本地分支中的提交(commit)已经被推送到远程仓库,就千万不要对该分支进行衍合操作。如果把衍合当成一种在推送(push)代码之前整理提交历史的手段,而且仅仅衍合那些尚未推送的本地提交,就没问题。如果衍合那些已经推送的提交,并且已经有人基于这些提交对象开展了后续开发工作的话,就会出现叫人沮丧的麻烦。

参考

快进式合并

当我们 git pull 拉取代码时,背后实际上是进行了一次“快进式合并”:

1
2
3
4
5
6
7
8
$ git fetch origin master:master
$ git merge origin/master

* 756ba83 - 特性分支版本2(SHA-1 不变)
|
* 915fe84 - 特性分支版本1(SHA-1 不变)
|
* e7ce3f8 - master 基准版本(祖先)

那么,究竟什么是“快进式合并(fast-forward merge)”?如果顺着一个分支走下去可以直接到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)。

快进式合并一般只用于同一分支内的代码合并。

非快进式合并

作为对比,如果加上 --no-ff 参数进行“非快进式合并(no-fast-forward merge)”,其祖先图谱如下:

1
2
3
4
5
6
7
8
9
$ git checkout master
$ git merge --no-ff feature-test

* ab900eb - 合并版本(注意这里!)
|\
| * 756ba83 - 特性分支版本2
| * 915fe84 - 特性分支版本1
|/
* e7ce3f8 - master 基准版本(祖先)

可见,合并后保留有分支历史痕迹,能看得出来曾经做过分支合并。为了保证版本演进的清晰,当分支合并回主干时(如上),建议都加上 --no-ff 参数。

参考

checkout 命令可以用于三种场景:

  • 切换分支
  • 创建分支
  • 撤销修改

本文只介绍第三种场景。

例子

如果我们想要撤销一个文件的本地修改,自然可以手工编辑恢复,但这样做实在是吃力不讨好。 checkout 命令可以帮助我们:

只撤销本地修改

修改文件后,使用 status 命令查看一下文件状态:

1
2
3
4
5
6
$ git status
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: /there/is/a/modified/file

Git 提示我们,对于未 add 进暂存区的文件,可以使用 git checkout -- <file> 快速撤销本地修改。

同时撤销本地和暂存区修改

那么,对于已 add 进暂存区的文件,如何撤销本地修改?还是先使用 status 命令查看一下文件状态:

1
2
3
4
5
$ git status
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: /there/is/a/modified/file

先取消暂存修改

Git 提示我们,可以使用 reset 命令取消暂存:

1
$ git reset /there/is/a/modified/file

取消暂存后,文件状态就回到了跟“例1”一样了:

1
2
3
4
5
6
$ git status
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: /there/is/a/modified/file

再撤销本地修改

这时按提示使用 checkout 即可:

1
$ git checkout -- /there/is/a/modified/file

这时工作目录就干净了:

1
2
$ git status
nothing to commit, working directory clean

可以看到,结合使用 resetcheckout 命令,可以撤销 index 和 working tree 的修改。

一步到位

那么有更便捷的、一步到位的办法吗?有,指定提交即可:

1
2
3
4
5
$ git status
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: /there/is/a/modified/file
1
$ git checkout HEAD -- /there/is/a/modified/file
1
2
$ git status
nothing to commit, working directory clean

那么 checkout 命令的全貌究竟是怎样的呢?

checkout 命令格式

checkout 命令的格式及描述如下:

1
2
3
git checkout [<tree-ish>] [--] <paths>...

Updates the named paths in the working tree from the index file (default) or from a named <tree-ish> (most often a commit, tag or branch)
  • 默认使用 index 暂存区的内容覆盖本地修改,如果不指定 <tree-ish> 参数。
  • 或者可以使用指定的提交、标记、分支版本覆盖本地修改。
  • 为了避免文件路径 <paths><tree-ish> 同名而发生冲突,在 <paths> 前用 -- 作为分隔。

checkoutreset

还记得在《git reset 命令回退版本》中介绍的 reset 命令吗?它与 checkout 命令之间有什么区别与关系?

区别

在这里介绍 reset 命令的另一种形式:

1
2
3
git reset [<tree-ish>] [--] <paths>...

This form copy entries from <tree-ish> to the index for all <paths>. (It does not affect the working tree or the current branch.)

checkout 命令的参数一模一样,区别是什么?

命令 操作目标 描述
checkout 工作目录(working tree) 用于撤销本地修改
reset 暂存区(index) 只用于覆盖暂存区

因此 git reset <paths> 等于 git add <paths> 的逆向操作。

如果企图用 reset 命令覆盖工作目录,是会报错的:

1
$ git reset --hard /there/is/a/modified/file
fatal: Cannot do hard reset with paths.

关系

After running git reset <paths> to update the index entry, you can use git checkout -- <paths> to check the contents out of the index to the working tree.

Alternatively, using git checkout [<tree-ish>] [--] <paths> and specifying a commit, you can copy the contents of a path out of a commit to the index and to the working tree in one go.

参考

git checkout
git reset
Git 教程 - 撤销修改

Git 相比 SVN 的其中一个卓越之处,就在于有各种“后悔药”可吃。其中一种“后悔药”叫做 reset 命令,相当好用。

三个工作区域

理解 reset 命令的前提是理解文件流转的三个工作区域:

  • 工作目录(working directory)
  • 暂存区域(staging area)
  • 本地仓库(repo)

命令

reset 命令有三种参数形式,本文只介绍最常用的一种:

1
2
3
git reset [<mode>] [<commit>]

Reset the current branch head (HEAD) to <commit>, optionally modifying index and working tree to match.

该命令用于回退本地仓库当前分支下的版本,并可以选择重置暂存区域、工作目录的修改。

mode 参数

mode 参数必须是以下五种中的一种:

--soft

HEAD Only

Git 本地仓库的版本回退速度之所以快,全因为 Git 在内部有个指向当前版本的 HEAD 指针,当你回退版本的时候,Git 仅仅是把 HEAD 指针往回移动。

--mixed

HEAD and Index

默认参数。除了回退本地仓库的版本,还会重置暂存区域(也称为 Index File 索引文件)。

这个默默无闻的 --mixed 参数其实很常见,每次运行 git status 时都会看到它的作用:

1
2
3
4
5
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

new file: /there/is/a/new/file
modified: README.md

由于该命令实在太常用了,因此会被设为 alias 以便使用:

1
2
$ git config --global alias.unstage 'reset HEAD'
$ git unstage

--hard

HEAD, Index, and Working Directory

终极武器,将包括工作目录在内的三个工作区域全部重置或回退,工作目录将重置得一干二净,慎用。

常见的做法是回退到上一个版本,连同工作目录,就像一切从未发生过一样:

1
2
3
4
5
$ git reset --hard HEAD~1
HEAD is now at ......

$ git status
nothing to commit, working directory clean

如果“回退前的版本”已经 push 到远程仓库,则不建议这么做。

--merge

待补充。

--keep

待补充。

commit 参数

commit 参数有三种常见形式:

SHA1

使用 SHA1 值回退到指定的版本,适用于 SH1 值已知的情况:

1
$ git reset 17ef24c

更常用的参数,适用于偷懒:

  • HEAD 表示当前版本(默认参数)。
  • 上一个版本为 HEAD^ ,上上一个版本为 HEAD^^ ,以此类推。
  • 上 100 个版本,简写为 HEAD~100
  • ORIG_HEAD 表示上一个 HEAD ,一般用于撤销上一次 reset 。(”reset” copies the old head to .git/ORIG_HEAD)

HEAD@{}

Git 在 1.8.5 版本之后,加入了 HEAD@{} 功能,它通过一个链表记录 HEAD 的移动路径,链表头部的 HEAD@{0}HEAD 指针。这个功能可以用于回退到一个早已忘记的提交。

这个功能一般配合 reflog 命令使用。

例子

更多例子参见 git help reset 的 EXAMPLES 部分。

参考

Git 中 HEAD 和 ORIG_HEAD 指针分别指的是什么?

在提交了若干更新之后,又或者克隆了某个项目,想回顾下提交历史,可以使用 git log 命令查看,或者推荐使用 git 自带的图形化工具 gitk

命令方式

定制输出格式 1

git log 的默认输出格式非常不便于查阅提交历史,使用时可以带上以下三个参数:

1
2
3
4
5
6
7
$ git log --graph --oneline --decorate
* e6f4e18 (HEAD, master) Merge branch 'master' of origin
|\
* | abfa93b 本地仓库的提交
| * 1f1c21d (origin/master) 远程仓库的提交
|/
* 17ef24c 基准版本

命令的输出形象地展示了提交历史,包括本地分支比远程分支领先了多少个提交版本。

定制输出格式 2

如果对输出格式还不满意,可以使用 --pretty 参数定制输出格式。但由于该参数的选项较多,推荐设置为别名(alias)使用:

1
2
3
4
5
6
7
8
$ git config --global alias.lg log --graph --pretty=format:'%Cred%h%Creset - %s %Cgreen(%cr) %C(bold blue)<%an>'
$ git lg
* e6f4e18 - Merge branch 'master' of origin (1 minutes ago) <Cyn>
|\
* | abfa93b - 本地仓库的提交 (2 minutes ago) <Pete>
| * 1f1c21d - 远程仓库的提交 (3 minutes ago) <John>
|/
* 17ef24c - 基准版本 (4 minutes ago) <Jerry>

格式化输出,代码着色,而且附上了作者、提交时间和祖先图谱。

筛选提交历史

当某个特性分支开发完成之后,我们想要筛选并看清将要合并到主干的是哪些代码,从而理解它们到底做了些什么,是否真的要并入。可以用 --not 选项屏蔽 master 分支,这样就会剔除重复的提交历史,看起来更清晰:

1
$ git log feature-cache --not origin/master

也可用于筛选出准备 push 到远程仓库的提交,例如:

1
2
3
4
$ git log --graph --oneline --decorate HEAD --not origin/master
* e6f4e18 (HEAD, master) Merge branch 'master' of origin
|
* abfa93b 本地仓库的提交

图形化方式

如果对输出格式还不满意,推荐使用 gitk 命令调用图形化工具查阅提交历史:

gitk

上半个窗口显示的是历次提交的分支祖先图谱,下半个窗口显示当前点选的提交对应的具体差异(其中右边 Patch 窗口显示当前提交的文件列表,左边 Diff 窗口显示每个文件的提交差异)。

参考

Git 基础 - 查看提交历史

要想 Git 用得爽,首先要安装与配置好。

如何选择版本?

Git 1.x

旧版本,不再维护。

Git 2.x

新版本,推荐使用。不向下兼容 1.x。

如何安装?

Windows

需要安装 Git for Windows 的客户端 msysgit。推荐使用 绿色便携版 ,优势如下:

  • 无需安装,无需写注册表,无需管理员权限;
  • 可以从任意目录运行,甚至 U 盘;

与安装版的区别:

  • 不提供右键上下文菜单(如 Git GUI HereGit Bash Here),因为该功能需要写入注册表;
  • 不修改环境变量 %path% ,因此无法在命令行工具 cmd 中直接运行 git.exegitk.exe,解决办法:
    • 推荐使用自带的 Git Bash (类 Unix Shell)或 Git Cmd 进行替代;
    • 或将 %GIT_HOME%\cmd 目录永久加入环境变量 %path% (如果只想在当前会话中临时使用,只需在 cmd 中运行 set path=%GIT_HOME%\cmd;%path% 即可),然后运行 git --help 测试配置效果;

Linux / Unix

使用包管理 apt-get 或 yum 即可。

OS X

Homebrew 是最快最便捷的安装方式:

1
$ sudo brew install git

如何配置?

配置提交作者

开始使用 Git 之前,第一件重要的事情就是配置提交作者,首先做如下检查:

1
$ git config -l | grep user

如果返回为空表示未配置,需要配置如下:

1
2
$ git config --global user.name "你的姓名"
$ git config --global user.email "你的邮箱"

现在,可以愉快的开始使用 Git 了。更多配置请参考 这里

配置格式化与空白

格式化与空白是许多开发人员在协作时,特别是在跨平台情况下,遇到的令人头疼的细小问题。由于编辑器的不同或者Windows程序员在跨平台项目中的文件行尾加入了回车换行符,一些细微的空格变化会不经意地进入大家合作的工作或提交的补丁中。不用怕,Git 的一些配置选项会帮助你解决这些问题。

core.autocrlf

Git 在你提交时自动地把行结束符 CRLF 转换成 LF,而在签出代码时把 LF 转换成 CRLF。假如团队成员只在 Windows 上写程序,可以关闭此功能,避免 Git 自动格式化代码后干扰代码对比:

1
$ git config --global core.autocrlf false

core.whitespace

Git 预先设置了一些选项来探测和修正空白问题,配置方法待补充。

常见问题

Git 长期使用上的一点不便,解决方案仅供参考。

配置工作目录

Git 默认使用程序运行目录作为工作目录,这会带来使用上的不便。解决办法是新建 .bashrc 文件:

1
$ vim ~/.bashrc

添加一行:

1
2
# 请将 /d/Repos/ 替换成你的仓库目录
cd /d/Repos/

即可自动切换到本地仓库所在目录。

配置 SSH 代理和密钥

另一个潜在的问题是运行 Git Bash 并拉取远程仓库提示错误:

1
Could not open a connection to your authentication agent.

这是因为 ssh-agent 未随 bash 一起启动。你可以每次都手工启动,或推荐编写脚本自启动。

新建 .bashrc 文件,并添加如下代码:

1
2
3
4
5
6
7
8
9
#!/bin/sh
if [ -f ~/.agent.env ]; then
. ~/.agent.env >/dev/null
kill -15 $SSH_AGENT_PID
fi

echo "Starting ssh-agent..."
eval `ssh-agent |tee ~/.agent.env`
ssh-add ~/.ssh/github_rsa

这将会自启动 ssh-agent 并添加指定私钥。 ssh-agent 是一个密钥管理器,运行 ssh-agent 以后,使用 ssh-add 将指定私钥交给 ssh-agent 保管,其它程序(例如 git)在需要身份认证的时候,可以将认证申请交给 ssh-agent 来代为完成整个认证过程。

那么以后每次运行 Git Bash 的时候,就会看到输出效果如下:

1
2
3
Starting ssh-agent...
Agent pid 8828
Identity added: ~/.ssh/github_rsa

参考

学习 Git 快一年了,感受良多,总结如下。

如何开始?

早年的时候,为了获取优质的项目和资源,注册了 GitHub 的账号,但仅仅简单用了 Star 和 Clone 功能。后来开始想在 GitHub 上面托管一些资源,陆续做了一些功课:

  • LinuxCast Git 视频教程,手把手指导,可惜教程不再维护了。
  • Git 官方文档,第一手官方材料,由浅入深,涉及 Git 各方面的内容,建议有实操经验的同学深入阅读。
  • GitHub 帮助文档,偏向实操,建议初学者阅读。而且里面涉及到一些 GitHub 特性(图形化操作、Social、Pages)可以与 Git 互补。

其实网上相关教程、博客、书籍很多,但建议初学者重心先放在:

  • 基础命令的实操(推荐 Git cheatsheet ,按功能分组命令,便于记忆)
  • 简单概念的理解(例如重点关注 Git 文件流转的三个工作区域及远程仓库,至于配置、分支(我知道分支是 Git 的杀手锏功能,但事实上很多人只用到一个 master 分支)、工作流等先统统忽略)

然后安装好 Git,选择好一个代码托管商(如国外 GitHub,国内 GitCafe),赶紧先跑起来,再好起来!如果你曾大致了解过 Git 这一门技术,你会发现这是属于“记忆型”的技术,需要多用才熟能生巧。

我的学习轨迹

第一轮

第一轮学习从零开始,大致如下:

  • 简单阅读了一些文档,用思维导图做成笔记。
  • 安装客户端,实操命令。
  • 把所做所得整理成 PPT,用自己的理解在团队内部做了一次技术分享,反响不错,还加深了自己的理解。

第二轮

第二轮的起因是因为想用 GitHub Pages 服务搭建一个静态博客写写文章。由于已经有了第一轮的沉淀,这回学习速度就很快了。还顺便学会了 Markdown 语法和 Hexo 博客搭建。

第三轮

第三轮是因为跳槽后新公司正好使用的是 Git,但由于团队成员用得都不熟练,因此利用空余时间进一步研究了 Git 的进阶内容:

  • 分支管理与团队工作流程
  • 冲突解决方案
  • stash、amend、reset、log 等实用命令

整个过程使用的是“INK 学习法”,并再次将理解的内容整理成 PPT 与团队分享。这轮学习的不同之处在于:

  • 以往都是个人使用 Git,使用的命令都很简单。当与团队一起使用时,问题规模不同,对工具的理解也会进一步加深。
  • 与第一次纯粹分享不同,这次侧重于推广我的方案,规范团队的工作流程,将知识转化为生产力。

总结

学习的轨迹应当是螺旋向上,难易度逐轮递增。每一轮的学习主题还应有所侧重,意图一口一个大胖子的行为会噎死自己 :)

如今 Git 对我来说不止是门技术,更是一种生活方式。除了工作中每天都要用到,通过它还“连接”了我与开源世界。

之前本博客是挂在 GitHub Pages 空间上的,但由于众所周知的原因访问速度一直很慢,甚至频繁收到无法访问的告警邮件,实在是不堪其扰啊。因此决定迁移博客到国内的空间,并且对部分资源进行 CDN 加速。

迁移 Pages 服务

很多国内的空间都支持部署静态博客,例如 GitCafeCodingSAE七牛、……

在此特别介绍 GitCafe,一个 GitHub 的国内版,但访问速度比 GitHub 快,其 Pages 服务免费且支持绑定自定义域名,就选它了。

如何创建 GitCafe Pages 服务?参考这里

如何将 Hexo 静态博客部署到 GitCafe 仓库?参考这里,我的配置如下:

1
deploy:
  type: git
  repo:
    github: git@github.com:cynthia903/cynthia903.github.io.git,master
    coding: git@git.coding.net:cynthia903/cynthia903,coding-pages
    gitcafe: git@gitcafe.com:cynthia903/cynthia903.git,gitcafe-pages

注意仓库地址须使用 SSH 而不是 HTTP 协议,这样在推送代码时就无须繁琐的输入账号密码了。

运行命令即可生成站点并推送部署:

1
hexo d -g

使用 CDN 加速

利用 CDN 服务商遍布全国甚至全球的 CDN 缓存节点,可以使得各地网民迅速的访问到网站资源。对于 Hexo 这种静态博客来说,使用 CDN 进行 HTTP 网页加速尤为合适,缓存命中率极高,减轻源站的访问压力,在博客达到一定访问量时可以考虑使用这种方案。

目前来说,只需对部分静态资源进行 CDN 加速即可,使用 CDN 公共库可以满足需求。什么是 CDN 公共库?引述自 cnbeta

CDN公共库是指将常用的JS库存放在CDN节点,以方便广大开发者直接调用。使用CDN公共库不仅可以为你节省流量,还能通过CDN加速,获得更快的访问速度。

目前国内一些比较大的 CDN 公共库:

由于 BootCDN 公共库的资源较全,在此推荐选用。

以 hexo 主题 jacman 为例,修改文件 themes\jacman\layout\_partial\after_footer.ejs,找到以下 JS 和 CSS:

1
2
3
4
<script src="<%- config.root %>js/jquery-2.0.3.min.js"></script>
<script src="<%- config.root %>js/jquery.imagesloaded.min.js"></script>
<script src="<%- config.root %>fancybox/jquery.fancybox.pack.js"></script>
<link rel="stylesheet" href="<%- config.root %>fancybox/jquery.fancybox.css" media="screen" type="text/css">

替换为:

1
2
3
4
<script src="http://cdn.bootcss.com/jquery/2.0.3/jquery.min.js"></script>
<script src="http://cdn.bootcss.com/jquery.imagesloaded/2.1.0/jquery.imagesloaded.min.js"></script>
<script src="http://cdn.bootcss.com/fancybox/2.1.5/jquery.fancybox.pack.js"></script>
<link rel="stylesheet" href="http://cdn.bootcss.com/fancybox/2.1.5/jquery.fancybox.css" media="screen" type="text/css">

即可生效。

在 JSP 标签中指定一个属性值 value 时,可以使用字符串:

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

也可以使用 EL 表达式:

1
<jsp:setProperty name="box" property="perimeter" value="${expr}"/>

那么,EL 表达式 ${expr} 中可以放些什么呢?

操作符

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

基本操作符

操作符 描述
. 访问一个 Bean 的属性或者一个映射条目
[] 访问一个数组或者链表的元素
() 组织一个子表达式以改变优先级

算术操作符

操作符 操作符 描述
+
- 减或负
*
/ div
% mod 取模

逻辑操作符

操作符 操作符 描述
== eq 测试是否相等
!= ne 测试是否不等
< lt 测试是否小于
> gt 测试是否大于
<= le 测试是否小于等于
>= ge 测试是否大于等于
&& and 测试逻辑与
|| or 测试逻辑或
! not 测试取反
empty 测试是否空值

隐式对象

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

作用域

对象 等价物 描述
pageScope this page 作用域
requestScope javax.servlet.http.HttpServletRequest request 作用域
sessionScope javax.servlet.http.HttpSession session 作用域
applicationScope javax.servlet.ServletContext application 作用域

HTTP 请求参数

对象 等价物 描述
param request.getParameter(...) 获取指定 HTTP 请求参数,字符串
paramValues request.getParameterValues() 获取所有 HTTP 请求参数,字符串数组

例如,要判断 HTTP 请求参数 from 是否为空,可以结合使用 JSTL <c:if> 和 EL 操作符 notempty、EL 隐式对象 param 进行判断:

1
<c:if test="${not empty param.from}">

HTTP 请求头

对象 等价物 描述
header request.getHeader(...) 获取指定 HTTP 请求头,字符串
headerValues request.getHeaders() 获取所有 HTTP 请求头,字符串数组

例如,获取请求来源:${header.Referer}

对象 等价物 描述
cookie request.getCookies() javax.servlet.http.Cookie 数组

例如,获取指定 Cookie 的值:${cookie.key.value}。这段 EL 表达式会被 JSP 容器解析成:

1
2
3
4
5
6
7
8
9
10
11
12
Cookie[] cookies = request.getCookies();
Cookie current = null;

for(Cookie cookie : cookies) {
if(cookie.getName().equals("key")) {
current = cookie;
}
}

if(current != null) {
out.print(current.getValue());
}

上下文

对象 等价物 描述
initParam 上下文初始化参数,即 web.xml<context-param>
pageContext javax.servlet.jsp.PageContext 提供对 JSP 页面所有对象以及命名空间的访问

函数

EL 表达式支持使用函数,其使用语法如下:

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

ns 指的是命名空间(namespace),fn 指的是函数的名称,param1 指的是第一个参数,param2 指的是第二个参数,以此类推。

例如,要获取一个字符串的长度,可以使用 JSTL 的 length 函数:

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

更多 JSTL 函数,参考这里

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

它的出现,是因为人们开始注重软件的分层设计,不希望在 JSP 页面中出现 JAVA 逻辑代码。同时也由于自定义标签的开发难度较大、不利于技术的标准化,因此产生了 JSTL。

JSTL 和 EL 的结合,基本可以让页面再无 <% %> 代码。

JSTL 标准标签库可分为五类:

核心标签库

共 14 个,从功能上可以分为 4 类。引用方法:

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

表达式控制

标签 描述
<c:out> 用于显示数据,就像 <%= %>,区别在于 <c:out> 标签可以直接通过 . 操作符来访问属性
<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:forEach> 基础迭代标签,接受多种集合类型
<c:forTokens> 根据指定的分隔符来分隔内容并迭代输出

URL 操作

标签 描述
<c:import> 检索一个绝对或相对 URL,然后将其内容暴露给页面
<c:url> 使用可选的查询参数来创造一个 URL
<c:redirect> 重定向至一个新的 URL
<c:param> 用来给包含或重定向的页面传递参数

格式化标签库

用于格式化并输出文本、日期、时间、数字,这里只介绍最最最常用的两个标签。引用方法:

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

格式化数字

标签 描述
<fmt:formatNumber> 使用指定的格式或精度格式化数字

格式化日期

标签 描述
<fmt:formatDate> 使用指定的风格或模式格式化日期和时间

SQL 标签库

不常用。引用方法:

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

XML 标签库

不常用。引用方法:

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

函数标签库

大部分都是通用的字符串处理函数,用于配合 EL 表达式使用。引用方法:

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() 移除首位的空白符

参考

JSP 标准标签库(JSTL)