前面数据库CRUD操作的例子中可以体会到,一些页面是动态显示的,所以为了动态显示,就在Servlet中形成页面将其返回到浏览器上渲染成页面。但是这样做耦合度比较高,因为前端的页面很可能为了提升用户体验而频繁发生改变,这样Servlet程序就需要修改,重新编译,重新部署,前后端的耦合度太高。那么能不能将页面展示部分的程序独立出来呢?或者动态的部分作为参数传入该独立的程序中。
和JDBC类似,对于驱动、URL、用户名和密码等等,这些应该写在配置文件中(不需要编译),程序只需要读取即可,当需要连接其他数据库时,只需要修改驱动和URL即可(修改配置文件),对核心程序不会造成太大的改动。
因此,JSP技术就诞生了,Java Sever Pages,基于Java语言实现的服务器页面,这样就不再通过核心Servlet代码来实现前端的页面,解耦合。JSP是JavaEE规范的一种,JSP文件通常存放在WEB-INF里面,保护JSP,不能通过URL路径直接访问到;但是也可以放在该目录外面。另外jsp文件的后缀是.jsp,这种后缀也是可以通过Tomcat安装目录下的conf/web.xml文件修改。
1. 第一个JSP
为了方便访问,新建Module,在项目里面,WEB-INF外面,创建一个最简单的JSP空页面。部署项目并启动服务器,在浏览器访问该页面。虽然前端页面是空白的,但是在06文章中提到,Tomcat安装目录下的work文件夹存放的是JSP文件翻译后的java文件以及编译后的class文件(注意,如果是用IDEA创建的项目,就需要在IDEA的目录下去找,不是在Tomcat安装目录下去找。)。也就是说JSP文件,本质上也是某种功能的Java程序,只不过这个功能为了更加解耦合,独立出来。
JSP文件的内容如下所示(可以看到自动生成的jsp和网页的框架一样):



打开翻译后的java文件,发现该文件是一个类文件,该类继承了org.apache.jasper.runtime.HttpJspBase类,其中_jspService方法中的部分代码如下所示:
1 | public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase |
查看Tomcat实现的该类的源码,该类是一个抽象类,并且该类继承了HttpServlet,即意味着jsp本质上也是一个Servlet,该类的部分代码如下所示,可以看到,index_jsp类作为servlet实现类,Tomcat会创建对象并调用service()方法,而该方法必定是HttpJspBase的service()方法,然后该方法再调用自己index_jsp实现的_jspService()方法:
1 | public abstract class HttpJspBase extends HttpServlet implements HttpJspPage{ |
1.1 JSP的执行原理
经过上面的分析,可以看到,浏览器上访问的路径虽然是以.jsp结尾,访问的是某个JSP文件,其实底层执行的是JSP文件对应的Java程序。
Tomcat服务器负责将.jsp文件翻译生成.java文件,并将.java源文件编译生成.class字节码文件。访问.jsp文件,底层仍然执行的是.class文件中的程序。Tomcat服务器内置了一个JSP翻译引擎,专门负责翻译JSP文件,编译.java源文件。
index.jsp会被翻译生成index_jsp.java文件,编译生成index_jsp.class文件。index_jsp这个类继承了HttpJspBase,而HttpJspBase又继承了HttpServlet,所以jsp就是Servlet,只不过职责不同,jsp的强项是做页面展示。这在一定程度上解耦合,也解释了为什么IDEA生成web项目默认创建一个jsp文件,就是作为首页展示的。
翻译后的index_jsp.java文件中的index_jsp类中的service方法部分代码如下所示:
1 | public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) |
可以看到,实际上就是将页面的内容以及相关的代码独立成一个功能类,达到解耦合。即jsp文件中的普通内容(如html、css、js等等)都是被当做普通字符串来处理的,在翻译之后的java文件中,以out输出流的形式将其输出到浏览器中渲染。
1.2 JSP第一次访问比较慢
- 启动JSP翻译引擎
- 需要一个翻译的过程
- 需要一个编译的过程
- 需要Servlet对象的创建过程
- init方法的调用
- service方法的调用……(后续访问只需要调用该方法即可)
JSP本质上就是Servlet,所以它也是单实例多线程环境下运行的一个Servlet对象。所以一般情况下,项目部署后,自己先提前访问一遍所有的jsp,方便提升用户体验。
那么Tomcat是怎么知道JSP文件需要重新翻译的呢?在访问jsp的时候,会记录一下该文件的最后修改时间(lastModified()方法),然后对比上一次访问该jsp时该文件的修改时间,如果二者不一致,则重新翻译并编译。
2. 小脚本
2.1 注释
因为JSP是作为页面展示用的,所以里面的内容主体上是前端的内容。这里的注释也就分为两种,一种是JSP注释,即不会被翻译引擎翻译到Java文件中(推荐)。
1 | <%-- 这是JSP中的注释 --%> |
另一种注释就是前端内容(html、css、js)的注释了,但是它仍然会被翻译到Java文件中,只不过在浏览器端被当做注释不被展示。
2.2 普通字符串翻译
JSP文件中所编写的所有的html、css、js都会被当做普通字符串原封不动地翻译到Servlet的service方法中的out.write("翻译到这里")部分,即输出到浏览器上。这些内容不需要特殊标签符号限制。
2.3 JSP的小脚本scriplet
小脚本即指的是一段Java程序,因为作为动态页面,所以需要程序来控制,这就是JSP中的脚本,小脚本可以有多个,之间存在顺序关系。脚本格式如下所示:
1 | <% |
小脚本中的内容会被翻译到Servlet的service方法中,所以必须要编写Java语句,以分号结尾。
所谓的JSP规范,就是SUN公司制定好的一些翻译规则,按照翻译规则进行翻译,生成对应的Java源程序。不同的Web服务器,翻译的结果是完全相同的,因为这些服务器在翻译的时候,都遵守了JSP翻译规范。
JSP简单案例如下所示:
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
3. 声明语法
前面的小脚本是将程序翻译到service方法中,那么怎么将其他内容,比如声明成员变量、静态变量等等翻译到service方法之外、Servlet类中呢?这就用到声明语法了。注意,声明块中的Java程序会被JSP翻译引擎翻译到service方法之外,声明块中不能直接编写Java语句,除非是变量的声明。语法格式如下:
1 | <%! |
案例如下所示:
1 | <%-- |
4. JSP的内置对象
这里所说的内置对象指的是可以直接在JSP小脚本中拿来使用的引用,即一些成员变量、局部变量或者传入的实参等等,即只能在service方法中直接使用的对象(当然也可以在service方法中传参的形式去调用其他方法,这样其他方法就可以使用了)。JSP中有九大内置对象(可以在JSP翻译之后的Java源码中看到):
1 | public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) |
| 对象名称 | 完整类名(接口名) |
|---|---|
| pageContext | javax.servlet.jsp.PageContext(页面域) |
| request | javax.servlet.http.HttpServletRequest(请求域/请求对象) |
| session | javax.servlet.http.HttpSession(会话域) |
| application | javax.servlet.ServletContext(应用域) |
| out | javax.servlet.jsp.JspWriter(标准输出流) |
| response | javax.servlet.http.HttpServletResponse(响应对象) |
| config | javax.servlet.ServletConfig(Servlet配置信息对象) |
| exception | java.lang.Throwable(异常引用,isErrorPage=”true”使用) |
| page | java.lang.Object(page=this,很少用) |
注意,除了应用域、会话域和请求域,JSP又提供了一个页面域,即pageContext对象,只在一个JSP页面中有效,覆盖范围最小。request和session两个域使用较多。
测试案例如下所示:
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |

4.1 pageContext对象
该对象只能在同一个JSP页面中共享数据,不能跨JSP页面。可以用转发机制来进行测试一下。用的较少,常用方法也就是setAttribute和getAttribute等等。可以看一下翻译之后的源码,该对象的主要作用就是初始化其余的内置对象,另外获取JSTL标签库写入的内容。
5. 表达式
上面说的是语句,但是有时候我们需要的是表达式,不是语句。比如要在页面上动态显示:登录成功,欢迎XX回来。这种该怎么做呢?目前为止有两种方法:
- 一种是在小脚本中以输出流的形式,动态打印。(即前面servlet中的方法)
- 一种是将静态内容以纯文本的形式打印出来,将动态内容以输出流的形式打印出来。
1 | <% |
除了之外,还有一种更为简单的方法,那就是表达式语法。如下所示:
1 | <%= |
上面的例子如下所示:
1 | 登录成功,欢迎<%= username %>回来 |
查看翻译后的Java程序,可以看到,二者没什么区别,即翻译引擎会自动将表达式语句转换为out.print(Java表达式);语句,即表达式语句法具有输出到浏览器上的功能。
1 | // 对应第二种方法 |
5.1 表达式案例:动态输出<h1>到<h6>标题字
采用Servlet的方法如下所示:
1 | <% |
那么既然表达式语法由自动输出的功能,可以将上面的out.print()语句省略即可,将字符串双引号去掉即可,注意,表达式语法自带输出功能,所以和前面的h标记中间要没有空格,否则不会将其作为一个完整标签来看待。
1 | <% |
这样,可以用JSP页面代替html页面,就可以动态获取项目名,然后将其拼接在form表单的action路径中了。
6. 指令
JSP中的指令是指导JSP的翻译引擎如何翻译JSP代码。共三个指令:
- page(页面指令)
- include (包含指令)
- taglib(标签库指令)
指令的使用语法格式如下所示,可以有多个指令,同一个指令的多个属性可以分开写:
1 | <%@指令名 属性名=属性值 属性名=属性值[ 属性名=属性值...]%> |
6.1 page指令
page指令的属性有很多个,可以在IDE中,光标选中标签,然后点击ctrl+p,查看参数提示。常用属性如下所示:
| 属性名 | 描述 |
|---|---|
| contentType | 设置JSP的响应内容类型 |
| pageEncoding | 设置JSP响应时的字符编码方式 |
| import | 组织导入 |
| session | 设置当前JSP页面中是否可以直接使用session内置对象 |
| errorPage | 错误页面 |
| isErrorPage | 是否是错误页面 |
| isELIgnored | 是否忽略EL表达式(见后续文章讲解) |
6.1.1 contentType
该属性设置JSP的响应内容类型,即告诉浏览器本内容可以以哪种文本格式来看待渲染,对应response.setContentType(属性值);,也可以附带字符编码方式,如:
1 | <%@ page contentType="text/html;charset=UTF-8" %> |
翻译之后的Java代码是response.setContentType("text/html;charset=UTF-8")。
6.1.2 pageEncoding
该属性设置JSP响应时的字符编码方式,即以哪种格式编码的,所以浏览器在获取到数据时要以对应的编码方式来解码,解码之后再按照上面的响应内容类型来对待数据。也是对应response.setContentType(属性值)中的编码方式charset。如:
1 | <%@ page pageEncoding="UTF-8" %> |
翻译之后的Java代码是response.setContentType("charset=UTF-8")。因为contentType属性设置的内容可以包含charset,所以一般这个不用写,直接在contentType属性中写即可。
6.1.3 import
前面提到了JSP翻译后,可以在service方法中,也可以翻译到servlet类中,那么怎么导入外部类呢?这就需要用到import属性。如下所示:
1 | <%page import="java.util.Date"%> |
翻译之后的Java代码是import java.util.Date;。
6.1.4 session
session属性设置本JSP是否可以直接使用session内置对象。用法如下所示:
1 | <%page session="true"%> |
通过前面的翻译源码可以看到,默认是没有session属性的(默认属性值为true),但是仍然创建了session对象,即session = pageContext.getSession();。也就是说,如果没有session,会自己创建session。
只有当session属性值为false的时候,才不会定义session局部变量(可以自己测试一下,查看翻译之后的源码),**注意,这里的不定义指的是根本不会创建这个变量,而不是getSession(false)**。
注意,session的内置对象,是没有session则自己创建session。那么如果没有session就不创建session,只获取当前已有的session或者null该怎么办呢?此时我们可以禁用内置对象session,自己创建session = pageContext.getSession(false)。
6.1.5 errorPage
我们可能会遇到这种情况,服务器端程序出现异常错误,这时候前端页面就会报错,那么这种情况对于程序员来说比较常见,但是对于客户来说就比较麻烦,他们没见过这种情况。所以一般来说,为了避免影响客户体验,如果程序出现错误,就跳转到指定页面,用errorPage属性指明跳转到的页面。语法如下所示:
1 | <% errorPage="asdf.jsp"%> |
通过对比两个jsp文件翻译之后的Java代码,发现errorPage属性对应下面的变化:
1 | pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true); |
6.1.6 isErrorPage
上个指令虽然在前端省去了异常堆栈信息,但是注意后台却没有异常堆栈信息,这样就会导致程序员无法排错,此时我们需要使用内置对象exception,手动将异常堆栈信息打印到指定的位置(日志),注意,是在跳转后的错误页面进行设置。首先设置isErrorPage属性为true,这样才能使用内置对象exception。默认不写表示为false。
1 | <%@ page contentType="text/html;charset=UTF-8" isErrorPage="true" %> |
6.2 include指令
在网页中有一些主体框架,例如:网页头、网页脚,这些都是固定不变的,我们可以将网页头、网页脚这些固定不变的内容单独抽取出来编写到某个JSP文件中,在需要的页面使用include指令将该JSP文件包含进来。该指令只有file一个属性,用于将其他文件内容引入到本文件中。
优点是代码量少了,便于维护,修改一个文件即可作用于所有的页面。在一个JSP文件中可以使用多个include指令。
注意,前面提到过,JSP文件会被翻译成.java文件,那么这个include指令是怎么工作的呢?是在翻译前将两个jsp文件整合到一起再翻译成一个java文件,还是分别翻译,然后再将Java文件整合到一起呢?
查看翻译后的文件发现,只有一个java文件,那么include指令是在编译期包含,先将文件整合,然后再翻译成一个java文件。在翻译器包含或者编译期包含,这被称为静态联编。但是这样,两个文件不能包含重名的变量,会造成冲突。
案例如下所示:
1 | <%-- index.jsp文件 --%> |
1 | <%-- index2.jsp文件 --%> |
注意,因为是将JSP文件整合到一个文件中(service方法中的局部变量或者servlet中的成员变量),最终翻译到一个service方法中或者同一个servlet类中,所以两个文件定义的变量是可以共用的。
6.3 taglib指令
见后续标签库部分。
7. 动作
JSP的动作指的是转发等操作(并不包含重定向),语法格式如下所示:
1 | <jsp:动作名 属性名=属性值 属性名=属性值...></jsp:动作名jsp:动作名> |
JSP的动作如下所示:

主要的动作有forward、include等等。
7.1 include
前面提到过,include指令可以静态联编,将JSP文件整合到一起再翻译成Java文件。而include动作则可以动态联编,分别翻译成Java程序,在调用包括的文件即可。

可以看到在主JSP文件翻译后的Java程序中多了下面的这句话:
1 | org.apache.jasper.runtime.JspRuntimeLibrary.include(request, response, "index2.jsp", out, false); |
即运行时将二者联合起来,实际上是在运行阶段调用,动态联编。允许两个文件包括重名的变量。
所以,如果两个文件不共享变量,但是有重名变量,这就需要动态联编;如果想共享变量,只能采用静态联编。
7.2 forward
转发动作和Java中的转发一样,没什么区别。转发动作语法如下所示:
1 | <jsp:forward page="index2.jsp"></jsp:forward> |
8. JSP总结
可以看到,目前JSP的初衷就是为了读取传过来的封装好的数据进行动态页面展示,可以自动翻译前端代码,也可以利用特殊语法嵌入后端代码。但是不得不说JSP规定了很多种语法,导致仍然出现了大量代码,前后端的分离并不是很理想。所以JSP后期改进,采用EL表达式+JSTL标签库来简化JSP中的Java代码,从而在一定程度上形成前后端解耦合。
9. EL表达式
上面总结中提到,JSP主要目的就是读取封装好的数据,然后将其输出到页面中,这部分代码比较繁琐,包括类型转换等等。那么可不可以将这些步骤简化呢?实际上是可以的,EL(Expression Language)表达式就是负责这部分,将这部分代码再封装。
EL表达式是一个由Java开发的工具包,专门用于从域对象读取数据并写入到响应体中,该工具包在Tomcat安装目录下的lib目录中(el-api.jar)。
简单语法如下所示:
1 | ${域对象别名.关键字} |
域对象的别名如下所示:
| 域对象 | EL表达式别名 |
|---|---|
| application | applicationScope |
| session | sessionScope |
| request | requestScope |
| pageContext | pageScope |
9.1 简单使用
在域中存储基本数据类型,简单获取如下所示:
1 | <% |

查看翻译后的Java文件,翻译成的Java语句如下所示:
1 | out.write((java.lang.String) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${pageScope.pageContext}", java.lang.String.class, (javax.servlet.jsp.PageContext)_jspx_page_context, null)); |
9.2 读取引用数据类型的属性数据
用法和上面类似:${域对象别名.key.属性名}。注意,EL表达式利用反射机制,通过调用当前属性的get方法来读取对应的属性值的,所以引用数据类型要有get、set等方法。测试案例如下所示:
1 | package el; |
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
9.3 简化版
可以更简单点,不写域对象别名,直接写key。但是这样做的原理是什么呢?他怎么知道是哪个域对象的呢?它是根据作用域的范围来选择的,首先对pageContext对象进行查找,如果找到关键字则输出,没有则对request对象进行查找,然后对session查找,最后对application对象查找。都没有找到则返回null。所以它首选是根据域对象的作用范围由小到大进行查找,然后小范围对象优先,所以如果采用简化版,key要尽量不同。
缺点:会增加检索时间,效率降低;另外key不同相同,否则会“覆盖”。所以,适合专门读取pageContext对象中的内容。
9.4 EL表达式中的运算
EL表达式支持算术运算(+-*/)、关系运算(>, >=, <, <=, ==, !=)和逻辑运算等运算(&&, ||, !)。注意,EL表达式没有if…else条件语句,所以在涉及到关系运算时可以采用条件表达式。简单案例如下所示:
1 | <% |

9.5 EL表达式其他内置对象
param
该对象表示
request.getParameter(),语法格式${param.请求参数名},等同于request.getParameter("请求参数名")。即读取请求对象中的参数内容。paramValues
该对象相当于
request.getParameterValues(),语法格式${paramValues.请求参数名},相当于获取类似复选框的这种一对多的属性的值,返回一个数组。注意,EL表达式不支持循环,所以对于数组这种就需要后续的JSTL标签库了。initParam
该对象相当于
application.getInitParameter(),读取应用域对象的一些全局配置信息(web.xml)。
简单案例测试如下所示:
1 | 用户名:${param.username}<br> |

9.6 EL表达式缺点
- 只能读取域对象数据,不能像域对象中写入数据和修改数据;
- 不支持控制语句(if、for、while)等等。
而JSP页面中应当尽量少出现Java代码,所以这就需要另一个工具JSTL标签库。
10. JSTL标签库
JSTL的全称是:JSP Standard Tag Lib,JSP的标准标签刻库。因为JSP中最好不要出现Java代码,所以出现了EL表达式简化Java代码,但是EL表达式存在一定的缺点,无法将所有的Java代码都转换成EL表达式,所以SUN公司又扩展了一些功能,将Java代码转换成标签形式。其实就是为了从表面看起来前后端分离,但是本质上仍然是Java代码,相当于EL表达式的扩展。
JSTL主要由以下四个模块组成:
- 核心标签:Java在JSP上的基本功能的封装(如if、while等等)
- SQL标签:JDBC在JSP上的使用
- xml标签:DOM4J在JSP上的使用(专门用于读取xml文件)
- Format标签:JSP文件格式转换(如日期转换成字符串)
其中,因为JSP现在越来越少用,所以主要是其核心标签用的比较多,主要讲解一下核心标签的用法。
10.1 配置JSTL
和JDBC.jar包一样,这个标签库属于扩展的功能,不属于Java SE,所以首先需要导入已经实现的标签转换Jar包:jstl.jar和standard.jar,这两个jar包可以在Tomcat自带的examples项目中找到(这里的jar包,需要在external libraries中导入,不是项目的web-inf/lib下,不知道二者的区别是什么上)。导入项目所需的jar包后,需要在jsp文件中引入JSTL中core包依赖约束,即上面所述的jsp的taglib指令。如下所示,其中uri是固定的,prefix指的是所用标签的一个前缀,用于区分不同人所写的标签,相当于python中的import xxx as后的别名,下面的c指的就是core的别名,后续用core中的标签,可以先写一个前缀c用于表示core中的标签。
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
10.2 set标签
set标签用于在四个域对象中存储内容,(从域对象中取内容已经在EL表达式中讲解了),语法如下所示:
1 | <c:set scpoe="session" var="key", value="value"></c:set> |
该句代替的是:
1 | session.setAttritube("key", "value"); |
其中scope指的是四个域对象别名的别名,这里的别名是将上述别名后面的Scpoe去掉。var指的就是键值对中的key,value指的是键值对中的value。
10.3 if标签
if标签用于替代Java中的if语句。语法如下所示:
1 | <c:if test="通过EL表达式进行判断"> |
10.4 choose标签
choose标签用于代替Java中的switch语句。语法如下所示:
1 | <c:choose> |
10.5 forEach标签
该标签用于替代Java中的循环,语法如下所示:
1 | // 第一种用法 |
10.6 案例
案例如下所示:
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |

11. 备注
参考B站《动力节点》。