Java JSP 笔记

JavaServer Pages(JSP)是一种帮助软件开发人员基于 HTML、XML 或其它文档类型创建动态生成的网页的技术。在结构上,JSP 可以被看作是一个高层次抽象的 Java Servlet。JSP 在运行时被转换成 Servlet,因此 JSP 是一个 Servlet;每个 JSP Servlet 都被缓存并重新使用,直到原始 JSP 被修改。

默认编码 UTF-8

Servlet 默认编码 UTF-8 的基础上,修改 $CATALINA_HOME/conf/web.xml,添加:

JSP 与 Servlet

JSP 是一个高层次抽象的 Servlet,为什么这么说呢?因为 JSP 在运行时会被容器转换为一个对应名称的 Servlet(JSP 编译器将 *.jsp 文本文件转换为 *.java servlet 源文件,然后再将其转换为 *.class servlet 类文件),因此实际运行时,是不存在 JSP 的,在 Tomcat 眼里只有 Servlet,包括静态资源。

在 $CATALINA_HOME/conf/web.xml 中,可以看到两个预定义的 servlet(default、jsp):

  • 当客户端访问 index.html 时,默认会被 default 这个 servlet 命中,default-servlet 做的事情很简单,就是去磁盘中读取这个 index.html 文件,然后添加适当的响应头部,发送给客户端就行了。
  • 当客户端访问 index.jsp 时,默认会被 jsp 这个 servlet 命中,jsp-servlet 做的事情也很简单,也是先去磁盘中读取 index.jsp 文件,然后根据预定义的规则将其转换为 servlet 源文件(当然与平常的 servlet 类有点不同),接着将其编译为 servlet 类文件,最后,如果是第一次执行那么先创建该 servlet 类的实例,执行它的 init 方法,然后执行 service 方法(基本步骤和 servlet 类似);客户端再次访问时,jsp-servlet 会检查 jsp 文件是否修改(对比它与对应的 servlet 源文件的时间戳),如果修改了,那么重复上面的转换、编译步骤,如果没有修改,那么直接执行这个 servlet 实例的 service 方法,当容器即将关闭时,jsp 对应的 servlet 对象被回收(调用 destroy 方法、回收内存)。
  • 当客户端访问 index.do 时,会根据 app/WEB-INF/web.xml 以及对应的 Web* 注解,匹配到对应 Servlet 类,如果是第一次访问,那么先创建实例,执行 init 方法,再执行 service 方法,下次访问时,直接执行 service 方法,当容器即将关闭时,对应的 servlet 对象被容器回收(调用 destroy 方法、回收内存)。

之所以使用 JSP 是因为在 Servlet 中编写 HTML 太繁琐了,总是需要键入 out.println(),经常需要转义。如果能像 PHP 那样在 HTML 文档中插入 PHP 代码就好了(Servlet 是典型的 Java 代码中插入 HTML 文档),JSP 就是在这样的背景下产生的。JSP 和 PHP 有很多类似的地方,但是 JSP 明显更复杂一点(语法更复杂些,JSP 有许多特殊的标签,而 PHP 只有一个)。但是有一点不同,PHP 页面每次被访问时都是要重新编译的(PHP-FPM),而 JSP 会在第一次访问时转换并编译为 Servlet 类,运行时与原始的 JSP 文件无关,如果 JSP 文件被修改,那么 Java 容器默认会重新转换并编译为 Servlet 类,然后释放并回收过期的 Servlet 实例(调用 destroy 方法),创建新的 Servlet 实例,执行 init 方法、service 方法。因此实质上,JSP 还是一个 Servlet,而不是一门新的脚本语言,所以除了第一次访问(以及修改后的第一次访问),其它时候 JSP 的性能与原始 Servlet 是一样的,为了提高客户端第一次访问时的体验(减少加载时间),通常会在 Tomcat 启动前对 JSP 文件进行预编译(并且禁止在运行期间修改 JSP 文件),这样一来和原始的 Servlet 就没有多大的区别了。

在 Tomcat 中,由 JSP 产生的源文件和类文件位于 $CATALINA_HOME/work/<engine>/<vhost>/<context>/org/apache/jsp 目录下,如果 jsp 文件名为 index.jsp,那么对应的 java 源文件就是 index_jsp.java,对应的 java 类文件就是 index_jsp.class。这个目录下的文件默认不会被自动删除,如果 jsp 文件没有被修改,那么 Tomcat 重启后,第一次访问 jsp 页面会很快,因为没有了转换、编译过程。不建议手动删除这些源文件和类文件,Tomcat 自己会管理(除非对应的 jsp 文件被删除或者被重命名了,为了减少空间的占用,可以考虑手动删除它们)。

index.jsp

index_jsp.java(简化后)

index_jsp.java 中有 3 个主要的方法:
void _jspInit():一般情况下方法体是空的,JSP 中不允许定义签名相同的方法(最好也不要重名)
void _jspDestroy():一般情况下方法体是空的,JSP 中不允许定义签名相同的方法(最好也不要重名)
void _jspService(request, response):方法内的代码其实就是 JSP 中 <% %><%= %> 标签的内容

_jspInit()_jspDestroy() 是预留给 JSP 编译器用的(比如一些隐藏的初始化、回收代码),如果要定义自己的初始化方法,可以在 <%! %> 标签中定义 jspInit()jspDestroy() 方法,_jspService() 方法不能在 <%! %> 中显示定义,否则 JSP 编译器在编译时会提示方法重复定义,例子:

test.jsp

test_jsp.java(简化后)

通过两个由 JSP 编译器生成的 *.java 文件可以得知,JSP 的 _jspService() 方法中预定义了 8 个对象:

  • requestjavax.servlet.http.HttpServletRequest:HTTP 请求对象
  • responsejavax.servlet.http.HttpServletResponse:HTTP 响应对象
  • configjavax.servlet.ServletConfig:Servlet 初始化参数对象
  • applicationjavax.servlet.ServletContext:Servlet 所在的上下文对象
  • pageContextjavax.servlet.jsp.PageContext:JSP 页面的上下文对象
  • sessionjavax.servlet.http.HttpSession:Session 会话对象
  • outjavax.servlet.jsp.JspWriter:响应体的输出对象
  • pagejava.lang.Object:指向当前对象(this

如果当前 JSP 页面为错误处理页(即在文件中声明了 <%@ page isErrorPage="true" %>,isErrorPage 的值默认为 false),那么还有一个内置对象 java.lang.Throwable exception,它表示接受到的异常对象(如果是直接访问这个页面,那么 exception 内置对象的值为 null)。

整个 JSP 文件大致可分为两类:普通数据、JSP 数据(特殊标签),对于普通数据(比如 HTML 文档),JSP 编译器只是简单的将它们通过 out.write() 方法输出,因此你可以将普通数据看作是 out.write() 语句,而对于 JSP 数据,则会根据预定义的规则将其转换为特定的 Java 语句,比如 <% %> 标签的内容会被原模原样包含在 _jspService() 方法体中,<%! %> 标签的内容则为生成的类文件的其它主体(比如方法定义、变量声明)。

由 JSP 编译器生成的类默认导入了 3 个包:

JSP 文件第一次被编译为 Java 源文件、Java 类文件是在 JSP 页面第一次被访问时进行的,此时 JSP 文件、Java 源文件、Java 类文件的 Modify 时间戳是一样的(Java 源文件、Java 类文件的时间戳会被刻意修改为一样的数值);第二次访问 JSP 页面时,JSP 引擎会检查 JSP 文件与 Java 文件的时间戳,如果发现 JSP 被改过,那么会调用 JSP 编译器将其编译为 Java 源文件,然后再调用 Java 编译器将其编译为 Java 类文件,回收老的 Servlet 实例,实例化新的 Servlet 类,最后才会调用 _jspService() 方法为客户端提供服务;如果 JSP 没有被改,那么直接调用 _jspService() 方法为客户端提供服务。

JSP 的性能瓶颈主要出现在第一次访问、修改后的第一次访问,因为需要使用 JSP 编译器将 JSP 源文件编译为 Java 源文件,然后又要调用 Java 编译器将 Java 源文件编译为 Java 类文件,最后还要进行实例化、初始化,才会执行真正有用的 _jspService() 方法。导致的结果就是访问时非常慢,经常需要 2 秒以上的时间,再加上网络的延迟,用户会感到非常不耐烦,会认为这个网站很垃圾,打开页面都这么慢(无辜躺枪)!

第二个问题其实很好办,因为我可以不修改它嘛,生产环境中基本都不会动的。但即使不修改,JSP 引擎也会在每次访问时检查它们的时间戳是否相同,以便重新编译。不过我们可以通过给 jsp 引擎传递 development=false 启动参数,来禁用这个检测,默认情况下这个值为 true。当然,在开发环境中不建议关闭 development 模式,因为经常需要修改 jsp 文件。打开 conf/web.xml 文件,修改为:

如果想消除第一次访问时的延迟,可以考虑在 Tomcat 启动前进行 JSP 的预编译,即将 JSP 转换为 Servlet 类,转换后就不需要 JSP 文件的存在了,可以放心的删除,不过这个方法感觉很麻烦,而且我试了很多次都没有成功,所以暂且不研究如何预编译 JSP 了,以后再说吧(也有一个土方法,那就是使用脚本一个个访问这些 JSP 页面,但 JSP 文件太多的话也很难应付)。

JSP 特殊标签

脚本程序
脚本程序其实就是 _jspService() 的方法体,使用 <% statements %> 表示,或者使用等价的 XML 语句 <jsp:scriptlet> statements </jsp:scriptlet>

JSP 声明
声明语句是当前 JSP 类的其它主体,比如类的方法定义、变量定义,使用 <%! statements %> 表示,或者使用等价的 XML 语句 <jsp:declaration> statements </jsp:declaration>

JSP 表达式
JSP 表达式可以是任何符合 Java 语言规范的表达式,但不能以分号结尾,JSP 表达式被作为 out.print() 的参数输出,使用 <%= expression %> 表示,或者使用等价的 XML 语句 <jsp:expression> expression </jsp:expression>

JSP 注释
JSP 注释相当于 Java 注释,注释不会出现在编译后的文件中(在 JSP 中就是 Servlet 源文件咯),使用 <%-- comments --%> 表示,注意不要与 HTML 注释搞混了,HTML 注释是文档的一部分,而 JSP 注释不是,最明显的区别是 JSP 注释不会出现在响应体中,而 HTML 注释会(查看网页源码可以看到)。

JSP 指令
JSP 指令用来设置 JSP 页面相关的属性,语法为 <%@ directive attr="value" %>,directive 主要有 3 个:

  • <%@ page ... %>:定义页面属性,比如页面编码
  • <%@ taglib ... %>:引入标签库的定义,如自定义标签
  • <%@ include ... %>:静态文件包含,类似 C 语言的头文件包含

page 指令常用的属性:

  • pageEncoding:指定页面的编码,如 pageEncoding="UTF-8"
  • contentType:指定文档的类型,如 contentType="text/html; charset=UTF-8"
  • errorPage:发生异常时要跳转的错误处理 JSP 页面,如 errorPage="URL of other JSP file"
  • isErrorPage:设置当前页面是否可以作为错误处理页面,值为布尔值,如 isErrorPage="true"
  • import:指定当前 JSP 页面要导入的 Java 类,多个逗号隔开,如 import="java.io.*, java.net.*"
  • session:是否在当前页面中启用 HTTP Session,默认为 true,可设置为 false 来关闭,如 session="false"
  • buffer:设置 out 对象的输出缓冲区大小,单位为 kb,none 表示关闭缓冲区,如设置为 8kb buffer="8kb"
  • autoFlush:设置 out 对象的输出缓冲区是否自动刷新,默认为 true,如果为 false,且缓冲区已满则触发异常
  • extends:告诉 JSP 编译器,生成的 Servlet 类应该继承 extends 属性指定的父类(属性值为全限定类名)
  • info:设置当前 JSP 页面的描述信息,如 info="This JSP Page Written By ZARA"
  • language:设置当前 JSP 页面的脚本语言,默认为 Java,也即 language="java"
  • isThreadSafe:当前页面是否为线程安全的,默认为 true,表示可以多线程同时访问,false 则同时只有一个线程访问
  • isELIgnored:是否忽略 EL 表达式,默认为 false,即启用 EL 表达式支持,如果设置为 true,则原样输出 EL 表达式
  • isScriptingEnabled:是否启用 JSP 脚本/声明/表达式,默认为 true,如果为 false,且使用了它们则会触发编译错误

taglib 指令用来引入自定义的标签库,比如 JSTL 标签库,语法:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
prefix 用来指定标签库的前缀,uri 则用来唯一定位一个标签库,它并不代表一个真实存在的网址。

include 指令则用来静态包含文件,所谓静态包含也就是在 JSP 被编译之前进行文件包含,与 C 语言的头文件包含类似。
语法:<%@ include file="文件相对 url 地址" %>,注意是相对 APP,如果没有以 / 开头则在当前目录下查找包含的文件。

JSP 行为
行为标签一般用来控制 Servlet 引擎,如动态文件包含、url 重写、重用 JavaBean 组件,语法:<jsp:action attr="value"/>。行为标签基本上是一些预定义好的函数,下表是一些常用的 JSP 行为标签:

  • jsp:include:RequestDispatcher.include() 页面包含
  • jsp:forward:RequestDispatcher.forward() 页面转发
  • jsp:element:代表一个动态创建的 HTML 元素
  • jsp:attribute:代表一个动态创建的 HTML 元素属性
  • jsp:body:代表一个动态创建的 HTML 元素内容
  • jsp:useBean:创建一个 JavaBean 组件
  • jsp:setProperty:设置 JavaBean 组件的值
  • jsp:getProperty:获取 JavaBean 组件的值

JSP 行为标签也称为 JSP 动作标签、动作元素,一般来说,JSP 指令元素是在编译之前进行的(静态的),而 JSP 动作元素一般是在运行时进行的(动态的)。动作标签使用 XML 元素表示,动作标签可以理解为 JSP 的内置标签,它们都以 jsp 前缀(prefix)开头。

jsp:useBean 动作元素有两个属性:id 属性和 scope 属性。

  • id 属性:id 属性是动作元素的唯一标识,可以在 JSP 页面中引用。动作元素创建的 id 值可以通过 PageContext 来调用。
  • scope 属性:scope 属性用于声明动作元素的生命周期。id 属性和 scope 属性有直接关系,scope 属性定义了相关联 id 对象的寿命。
    • page:生命周期为当前页面,关闭、重新打开、刷新后变量或对象被重置。这是默认 scope 值。
    • request:生命周期为当前请求,因此 forward 跳转的几个页面内都有效,但是 sendRedirect 跳转的无效,因为是不同的两个请求。
    • session:生命周期为当前会话。
    • application:生命周期为当前应用。

如何理解 scope 属性呢?scope 的四个作用域其实都分别代表一个对象:

  • page -> javax.servlet.jsp.PageContext
  • request -> javax.servlet.http.HttpServletRequest
  • session -> javax.servlet.http.HttpSession
  • application -> javax.servlet.ServletContext

这几个类都有 getAttribute()、setAttribute() 方法,类似关联数组,使用 key 来获取对应的 value。而绑定到对应 scope 的对象其实都是调用的 setAttribute() 方法进行存储的,所以它们的生命周期其实也就是对应的实例的生命周期,从 _jspService() 方法中看得出,pageContext 实例是一个 final 变量,也就是它是栈变量,每次调用都会是不同的值;其它 3 个就比较好理解了,不再复述。

jsp:include 动作用来动态的包含页面内容,它与 <%@ include ... %> 的区别后面会详细说明,这里提一下它的语法:
<jsp:include page="相对 URL 地址" flush="true"/>
page 属性用来指定要包含的页面 url,如果以 / 开头,则为绝对路径(相对于 APP),如果不以 / 开头,则为相对路径(相对于当前路径)。
flush 属性表示在 include 之前是否先刷新当前 out 对象的输出缓冲区,这个不是很重要,缓冲区满了默认会自动刷新的,默认值为 false。

既然提到了输出缓冲区刷新,就来深入的研究一下刷新后有什么效果,又会有什么影响,默认情况下,输出缓冲区只会在缓冲区满时、service() 方法结束时被自动刷新。这是用来测试的 Servlet 类,建议使用 curl、telnet、netcat 来测试,方便观察停顿时间,停顿前后都输出了什么。

测试结果(双横线是我自己打上去的,用来分辨两次 sleep()

从结果中看得出,在刷新之前,设置的响应头、写入的响应体都没有被发送到客户端,它们都还存在于缓冲区中;在刷新之后,缓冲区中的数据(响应头、响应体)被发送到客户端,此后尝试添加响应头是不奏效的,其实这很好理解,因为客户端从接受的数据中就已经认为响应头结束了(因为已经接到了部分响应体),服务端是不可能往客户端的响应头中插入新的响应头了。顺便说明一下,Servlet 的输出缓冲区一般为 8192 字节,貌似手动 setBufferSize() 的值如果小于 8192 会没有任何效果,会被容器给忽略。

jsp:forward 动作其实就是 apache/nginx 中的 rewrite 功能,rewrite 与 redirect 的区别是,前者不会改变浏览器地址栏,因为这是一次 HTTP 访问,后者会改变浏览器地址栏,因为这是两次 HTTP 访问。forward 意味着当前页面将控制权转交给了新页面,因此 forward 之后的语句都是无效的,因为控制权已经不在当前页面上了。
语法:<jsp:forward page="相对 URL 地址"/>,page 属性的格式以及意义同 jsp:include 中的 page 属性。

<%@ include ... %><jsp:include ... /> 区别

  • <%@ include ... %> 是静态包含,类似于 C 语言的头文件包含,JSP 编译器会直接用对应的文件内容替换该语句,然后再进行编译。
  • <jsp:include ... /> 是动态包含,即运行时动态的包含指定 url 页面的内容,实际上它是调用 RequestDispatcher.include() 方法。

include.jsp

part-1.txt

part-2.txt

访问 /include.jsp

include_jsp.java 文件

动态创建 HTML 元素

JSP 动态创建 HTML 元素

JavaBean 是什么?
JavaBean 是符合一定规范的 Java 类,所谓规范也就是约定,只要符合这 3 条约定的 Java 类就可以称为 JavaBean:

  1. 所有属性都是私有的(提供 getter/setter 方法)
  2. 拥有一个 public 的默认无参构造函数
  3. 实现了 java.io.Serializable 接口

对于非布尔类型的属性 xxx,使用 getXxx()、setXxx() 方法;
而对于布尔类型的属性 xxx,可使用 isXxx()、setXxx() 方法。

比如 StudentBean 类:

符合 JavaBean 规范的类具体有什么用途呢?

参考来源:Java 帝国之 Java bean (上)Java 帝国之 Java bean(下)

我一手创立的 Java 帝国刚刚成立不久,便受到巨大的打击,我派出去占领桌面开发的部队几乎全军覆没。情报说微软的 Visual Basic 和 Borland 的 Delphi 最近在宣传什么组件化开发,难道就是这东西把我们搞垮了?

我赶紧买了一个 Visual Basic 过来研究,果然,这个家伙确实是方便,最让我惊叹的是:它有一个可视化编辑器!我只需要把一个组件(例如按钮)拖拽到一个表单上,设置一下属性(颜色,字体),再添加一个事件(onClick),最后在 onClick 中写点代码就搞定了!

不仅如此,我还可以把我的代码按规范包装成一个组件,发布出去让别人使用。我看着手下给我买来的《程序员大本营》光盘,里边竟然包含了好几千个这样的组件,有数据库浏览组件,计时器组件,颜色选取组件,甚至还有收发邮件的组件……

天哪,这以后开发桌面程序岂不太简单了!怪不得我的 Java 被打得满地找牙!我赶紧打电话给我的干将小码哥:小码啊,你赶紧看看这个 Visual Basic 和 Delphi,给你 7 天时间,我们 Java 也得赶紧搞一套这样的东西出来。

小码毫不含糊,三天就给我搞了一个东西出来:Java Bean API 规范。我翻开一看,哇塞,长达 114 页,于是问他:“这是什么东西?我要的可视化编辑器呢?Visual Java 呢?”

他说:“老大,我们是个开源的社区,得充分利用大家的力量,所以我没有去做像 VB 和 Delphi 那样的东西,相反,我定义了一套规范,只要大家按照这个规范做,谁都可以用 Java 做出像 VB 那样的可视化开发工具出来。”

“那你说说这个 java bean 到底是什么规范?”我问。

“首先,一个 java bean 其实就是一个普通的 java 类,但我们对这个类有些要求:”

  1. 这个类需要是 public 的,然后需要有个无参数的构造函数。
  2. 这个类的属性应该是 private 的,通过 setXxx() 和 getXxx() 来访问。
  3. 这个类需要能支持“事件”,例如 addXxxListener(XxxEvent e),可以是 click 事件,keyboard 事件等。
  4. 我们得提供一个所谓的自省/反射机制,这样能在运行时查看 java bean 的各种信息。
  5. 这个类应该是可以序列化的,即可以把 java bean 的状态保存的硬盘上,以便后来恢复。

“这些要求看起来也没啥啊,对程序员来说,不就是个普通的 java 类吗?到底该怎么用?”

“我们幻想一下,假设我们的 Java bean 大行其道了,有个用户在用一个 Visual Java Builder 这样的可视化开发工具,当他用这个工具创建应用的时候,可以选择一个叫 JButton 的组件,加到一个表单上,此时 Visual Java Builder 就需要把这 JButton 的类通过反射给 new 出来,所以就需要一个无参数的构造函数了。”

“如果用户想去设置一下这个 JButton 的属性,Visual Java Builder 就需要先用自省/反射来获取这个 JButton 有哪些属性(通过 getter/setter),拿到以后就可以给用户显示一个属性清单了,例如背景色,字体等等。用户看到后就可以设置背景色和字体了,此时 Visual Java Builder 在内部就需要调用这个 Bean 的 setBackgroundCorlor()、setFont() 等方法,这就是所谓的 setXxx()方法。”

“如果用户想对这个 JButton 编程, Visual Java Builder 还是通过自省/反射来获取这个 JButton 有哪些事件,给用户展示一个事件清单,例如 click、keyboardPressed 用户可以选取一个,然后就可以写程序对这个事件编程了。”

“可是那个序列化有什么用呢?”

“这是因为用户设计完了以后,可能关掉 Visual Java Builder 啊,如果不通过序列化把设计好的 JButton 保存起来,下次再打开 Visual Java Builder,可就什么都没有了”

我想了想, 小码哥设计的不错,仅仅用了一个简单的规范就满足了可视化编辑器的所有要求。

“那我们就发布这个规范吧,咱们自己先做一个可视化编辑器,给别人做个榜样,名称我都想好了,叫 NetBean 吧。”

果然不出我们所料,Java bean 发布以后,有力的带动了 Java 的 IDE 市场,开发 Delphi 的 Borland 公司也来插了一脚,搞出了一个 JBuilder,风靡一时。IBM 搞了一个 Visual Age for Java,后来摇身一变,成了一个叫 Eclipse 的开放平台,超级受大家欢迎,它反过头来把我们的 NetBean 和 JBuilder 逼的快没有活路了。

虽然我们玩的很欢,但是程序员根本不买账,Java 在桌面开发市场还是没有起色,使用 Java bean 创建桌面程序的程序员少之又少,只有部分像金融、ERP 这样的领地还在坚持。看来是无药可救了。但是 Java bean 何去何从?丢弃掉太可惜了,我和小码哥商量了一下,我们觉得:既然我们 Java 统治了服务器端的编程,还是在那里想想办法吧……

JSP 中使用 JavaBean
jsp:useBean 语法:<jsp:useBean id="beanName" class="package.className" scope="page"/>

  • id:javabean 对象的标识符,可以在其他地方引用。
  • class:javabean 对象的全限定类名,如 org.java.bean.TestBean。
  • scope:javabean 对象的生命周期,取值有 page(默认)、request、session、application。

jsp:setProperty 语法:<jsp:setProperty name="beanName" property="propName" value="propValue"/param="paramValueAsPropValue"/>

  • name:javabean 对象的标识符,该标识符需要与某个 jsp:useBean 的 id 属性对应。
  • property:指定要设置的属性名称,如果设置为 *,则表示用请求参数填充同名的属性。
  • value:手动为 property 属性指定一个值,该属性是可选的,但注意不要与 param 一起使用。
  • param:将请求参数 param 的值作为 property 属性的值,该属性是可选的,不可与 value 同用。
  • 如果 value 和 param 属性都没有提供,则表示将请求参数中的同名 param 的值赋给 property 属性。

jsp:getProperty 语法:<jsp:getProperty name="beanName" property="propName"/>

  • name:javabean 对象的标识符。
  • property:要获取的 bean 属性名称。
  • jsp:getProperty 元素获取的值会被作为 out.write() 的参数输出。

例子:
login.html

receive.jsp

ValidateBean.java

JSP 中使用 JavaBean

JSP 中的流程控制语句
前面说了 JSP 中的普通数据其实就是 out.write() 语句,因此我们可以将流程控制语句穿插在 <% statements %>html elements 之间,例子:

JSTL 标准标签库

JSP 标准标签库(JSTL)是一个 JSP 标签集合,它封装了 JSP 应用的通用核心功能。JSTL 支持通用的、结构化的任务,比如迭代,条件判断,XML 文档操作,国际化标签,SQL 标签。除了这些,它还提供了一个框架来使用集成 JSTL 的自定义标签。根据 JSTL 标签所提供的功能,可以将其分为 5 个类别:

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

在 Tomcat 中使用 JSTL 需要导入 jstl-1.2.jar 包,下载地址:jstl-1.2.jar,将下载到的 jstl.jar 包放在项目的 WEB-INF/lib 目录下即可,如果想要让所有 APP 都可以使用 JSTL,请将 jstl.jar 放到 $CATALINA_HOME/lib 目录下(建议这么做)。

因为 JSTL 不属于 JSP 核心标签,所以在 JSP 文件中使用 JSTL 需要先使用 <taglib ... /> 来导入它们,有 4 个常用的 JSTL 库:

  • core:核心标签库,<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
  • fmt:格式化标签库,<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
  • sql:数据库标签库,<%@ taglib prefix="sql" uri="http://java.sun.com/jsp/jstl/sql" %>
  • xml:xml 标签库,<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %>

prefix 不需要与上面的一样,你可以使用任意你喜欢的 prefix,但是按照惯例来总是好的。其中用的最多的是 core 库,fmt 偶尔用下,sql 和 xml 库基本没人用了,过时了。JSTL 标签参考:JSP 标准标签库(JSTL)- 菜鸟教程JSTL 标签大全详解 - CSDN 博客

自定义标签

JSP 自定义标签

表达式语言

JSP 表达式语言(EL)使得访问存储在 JavaBean 中的数据变得非常简单,EL 既可以用来创建算术表达式也可以用来创建逻辑表达式,在 EL 表达式内可以使用 整数浮点数字符串字面量(单引号或双引号包围)、true/false 以及 null。EL 不局限于 JavaBean 标签,实际上它可以用在任意非 <% %><%= %><%! %><%@ %> 元素中(这些都属于静态元素,还轮不到 EL 表达式的计算)。

EL 表达式的语法:${expression},其中的 expression 就是表达式的内容,和 shell 的变量引用有点类似。先看一个简单的例子:
test.jsp

curl 访问

EL 提供了两种方式来访问对象的属性:object.propertyobject['property']object["property"](单引号和双引号没有区别)。其中,object.property 的 property 必须符合 Java 标识符规范(以字母、下划线、美元符开头,后可接字母、数字、下划线、美元符);而 object[‘property’]、object[“property”] 的 property 则没有要求,可以是任意有效的字符串。补充一点,如果要进行动态取值(也就是 property 是动态确定的),那么只能使用后者,这和 JavaScript 是一样的。例子:

表达式的意思是,从 key 这个请求参数中获取 name 的请求参数名称,因为是动态确定的,所以只能使用 [] 访问符。

EL 表达式有 11 个隐含对象:

  • pageScope:page 作用域
  • requestScope:request 作用域
  • sessionScope:session 作用域
  • applicationScope:application 作用域
  • param:请求参数(单个),字符串
  • paramValues:请求参数(多个),字符串数组
  • header:请求头部(单个),字符串
  • headerValues:请求头部(多个),字符串数组
  • cookie:请求的 Cookie
  • initParam:context 初始化参数
  • pageContext:当前页面的 pageContext

访问数组只能使用 [] 操作符,因为索引值不是合法的 Java 标识符。比如 ${paramValues.name[0]} 获取第一个 name 请求参数的值。如果 expression 中出现了变量,如 ${username}、${age + 10},那么 EL 解释器会依次从 pageScope、requestScope、sessionScope、applicationScope 中尝试获取它们的值,只要找到了同名变量,就返回,如果都没找到则返回空字符串。

pageContext 对象的常用属性:

  • ${pageContext.request.method}:请求方法
  • ${pageContext.request.protocol}:HTTP 协议
  • ${pageContext.request.remoteAddr}:客户端 IP
  • ${pageContext.request.remotePort}:客户端 Port
  • ${pageContext.request.requestURL}:请求的 URL,不包括查询字符串
  • ${pageContext.request.queryString}:请求的查询字符串
  • ${pageContext.request.contextPath}:当前上下文路径(项目名称)
  • ${pageContext.session.id}:session ID
  • ${pageContext.session.new}:是否为新会话

EL 表达式支持的运算符:

  • .:属性访问符(静态)
  • []:属性访问符(动态)
  • ():子表达式,改变优先级
  • +:加
  • -:减
  • *:乘
  • /div:除
  • %mod:取余
  • ==eq:是否相等
  • !=ne:是否不等
  • <lt:是否小于
  • <=le:是否小于等于
  • >gt:是否大于
  • >=ge:是否大于等于
  • &&and:逻辑与
  • ||or:逻辑或
  • !not:逻辑非
  • empty:是否为空值

EL 的逻辑表达式返回 true 或 false 字符串,如 ${4 > 3 and 5 < 6} 返回 true。

JSTL 与 EL 的联系与区别
JSTL 是 JSP 标准标签库,使用 JSTL 需要引入 jstl.jar 包,而 EL 是表达式语言(实际上只能称为表达式,算不上脚本语言),使用 EL 不需要引入额外的 jar 包,JSP 容器默认支持(JSP 2.0 规范),EL 表达式是用来显示数据的,功能和 <%= expression %> 一样,只不过后者使用的是 Java 语言。JSTL 和 EL 的目的都是为了减少 JSP 页面中 Java 代码的使用,便于分层控制。