JSP的本质
JSP本质上就是Servlet, 正常情况下, 它会在第一次被访问的时候被容器转化成Java代码, 然后再从Java代码编译成.class文件, 之后实际就和Servlet没区别了, 也就是多个请求也只有这一个实例, 最终仍然是通过多线程单实例的方式来完成任务. 所以其实就算你写的JSP代码有问题, 也只有在用户访问它的时候才能看到, 项目部署的时候是看不到的. 当然某些容器提供部署的同时完成代码编译加载的功能, 默认是不开启的.
JSP中几种常见的元素
JSP其实就是html中带了一些其他的元素以实现表现层(viewer)的功能, 下面是一些常见JSP元素 :
1. Scriptlet : <% %> : 里面可以写Java代码, 这里的Java代码在最后会被统一移到生产的类Servlet类的类似serveice的方法里面 :
<% System.out.println(123); %>
2. 表达式 : <%= %> : 里面的内容将作为out.println()的参数, 被打印在页面上 :
<%=
config.getInitParameter("name")
%>
3. 声明 : <%! %> : 里面可以声明类的属性和方法, 用来弥补scriptlet中声明变量的均为局部变量的缺陷
<%!
private static int x = 3;
private void print(){
System.out.println(123);
}
%>
4. 指令 : <%@ %> : 向容器提供一些特殊的指示 :
这里例子是page指令的import 和 contentType属性, 还有其他属性, 同时其实指令还有include和taglib, 之后有机会再提.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.LinkedList" %> : 导入你需要在这个Jsp/Servlet 里面使用的包
JSP中的Servlet
上面说到JSP本质上就是Servlet, 那么也就是Servlet里面的一些东西它也有 :
隐式对象
在Scriptlet内部的一些常用的隐式对象(这里要注意的是只能在Scriptlet内部也就是那个类service方法的内部使用, 因为这些变量实际上都是在其内部定义的) :
- out : Servlet中需要通过resp.getWriter()来获得.
- request, response : Servlet中作为doGet/doPost/... 等方法的参数传入.
- session : 即Servlet中的HttpSession, 在Servlet通过req.getSession()来获得.
- application : Servlet中通过getServletContext()来获得
- config : Servlet中通过getServletConfig()来获得. (没错, JSP也可以设置初始化参数, 具体设置方法在后面提到)
- pageContext : 里面包含了其他隐式对象的引用
可以覆盖的方法
下面是可以覆盖的几个方法, 其作用和Servlet相同就不多说了 :
<%!
@Override
public void jspInit(){
}
@Override
public void jspDestroy(){
}
%>
JSP初始化参数的设置
然后来看看JSP如何设置初始化参数 :
<servlet>
<servlet-name>indexJsp</servlet-name>
<jsp-file>/test.jsp</jsp-file>
<init-param>
<param-name>name</param-name>
<param-value>index.jsp</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>indexJsp</servlet-name>
<!--<url-pattern>/xixi</url-pattern>-->
<url-pattern>/test.jsp</url-pattern>
</servlet-mapping>
可以看到其实和Servlet大同小异, 要注意的是, 这里如果mapping中url-pattern不用jsp的实际路径而是使用虚拟路径的话, 会出现一个问题, 如果用户知道真实的jsp的路径, 他仍然可以访问, 但是那个jsp并不会获得初始化参数. 必须要使用url-pattern对应的路径才能获得初始化参数. 由于行为逻辑基本与Servlet相同, 所以在执行jspInit的时候已经可以使用getServletConfig()来获得初始化参数了.
属性的设置以及获取 :
类似于Servlet, 我们可以通过以下隐式对象来设置属性 :
- application 对应 ServletContext
- session 对应 HttpSession
- request 对应 HttpServletRequest
- 这里多了一个pageContext 也可以用来设置属性, 它设置的属性只在该page内共享, 但它也能设置/查找上面三者的属性, 具体规则就不表了.
JSP的转折
令人遗憾的是, 上面所说的大部分JSP元素, 实际上都是不推荐使用的 : 例如Scriptlet, 表达式 和声明(这三者均属于脚本代码), 因为这些东西使得Web的前后端之间无法很好地分离, 甚至在web.xml中存在禁用这三种元素的标记 :
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<scripting-invalid>true</scripting-invalid>
</jsp-property-group>
</jsp-config>
一旦设置这个标记之后, 服务器部署正常, 但是一旦访问使用了上述元素的JSP网页, 网页直接HTTP 500. 那我们该使用什么呢? 答案是标准动作 + EL(expression language) + JSTL标记...
标准动作
首先要对bean有个基本的认识, 如果没有认识百度一下吧, 这里我个人由于接触的比较多就不提了, 由于在Java中bean没有在语言层面上实现所以有点麻烦. 这里我使用的Student类的源代码 :
package beanPackage;
public class Student {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
JSP中总共有三个bean标准动作 :
1. <jsp:useBean id="s" class="beanPackage.Student" scope="application" /> 这里scope指的是查找的范围, 默认为page, 可以设置为application, session, request, page分别对应上面提到的jsp中的4中属性设置的方式, 而你属性设置中其实是一个键值对, 这里的键就对应该标签中的id, 由于设置的属性均为Object, 所以还需要有class来识别其class, 而且这里的class必须使用完全限制名, 在上面使用page指令的import属性导入模块是无效的. 要注意的是, 这里之所以要这三个属性, 实际上是因为最终JSP还是要转化为Java代码的. 这句话也就会转化为类似 : beanPackage.Student s = (beanPackage.Student)application.getAttribute("s")的话.
2. <jsp:getProperty name="s" property="name"/> 这里的s对应上面已经获取到的Student的id, property对应Student中的name属性. 这句话会在html中打印这个student的名字.
3. <jsp:setProperty name="s" property="age" value="21"/> 这里s对应上面获取到的Student的id, age对应Student中的age属性. 这句话会将这个Student实例的age改为21.
附加 :
关于useBean
实际上useBean总是会返回一个bean对象, 就用上面的例子来说<jsp:useBean id="s" class="beanPackage.Student" scope="application" />, 如果在application内没有这个Student, 那么它会利用bean的无参数构造函数自动构造一个然后添加到application当中. 基于这个特性, 实际上这个标签的还可以写为 :
<jsp:useBean id="s" class="beanPackage.Student">
<jsp:setProperty name="s" property="name" value="Fred" />
<jsp:setProperty name="s" property="age" value="21" />
</jsp:useBean>
useBean内部的内容只有当没有在作用域内找到该对象时才会执行.
实际上jsp:useBean之中还可以设置type, 例如你想到生成的对象是Human s = new Student()的时候, 那么这句话可以写为
<jsp:useBean id="s" type="beanPackage.Human" class="beanPackage.Student" scope="application" />
另外如果只有type没有class, 此时如果能在作用域内找到对象, 那么这个对象的引用将是type中指定的类型, 如果不能找到, 直接报错.
关于setProperty
实际上setProperty中还可以设置param :
<jsp:useBean id="k" class="beanPackage.Student">
<jsp:setProperty name="k" property="name" param="name" />
<jsp:setProperty name="k" property="age" param="age" />
</jsp:useBean>
这个param能直接从request的参数中寻找名字为name和age的参数并且赋值给我们此处的Student实例. 更牛逼的是, 如果你request参数的名字和你实际Property的名字相同的话, 就比如上述例子中, 则可以直接省略param :
<jsp:useBean id="k" class="beanPackage.Student">
<jsp:setProperty name="k" property="name" />
<jsp:setProperty name="k" property="age" />
</jsp:useBean>
当然最牛逼的还是这个, 使用*, 这种情况下容器会自动迭代所有request中的参数来尝试匹配你的bean的property, 并设置为对应的值.
<jsp:useBean id="k" class="beanPackage.Student">
<jsp:setProperty name="k" property="*" />
</jsp:useBean>
其实这里还有一点, 我的age的类型是int, 但是request传入的所有的参数值都为String, 但是仍然能够匹配, 这是因为容器提供了基本类型和String的自动转换. 另外如果我们实例的类有父类, 父类的属性也会得到匹配.
EL
EL在JSP 2.0 已经成为规范的一部分, 它有自己的语法, 属于另外一类JSP元素. 当然也有忽略EL的方法, 这里不表. 有了标准动作为什么需要EL呢? 就一个很简单的例子, 比如上面的Student有一个性质是Book, 我们想要显示这个Book的name :
<jsp:getProperty name="k" property="book"/> --> 会打印 book.toString
这时候如果用el的话就是 :
${k.book.name}
el表达式实际上很简单 : 任何语句都包含在${}
当中, 然后可以用.
和[]
来取值, 用点时, 左边只能是map或者JSP四个作用域中的某个作用域的属性(某个bean), 而右边必须是map的键或者是bean的Property. []
则使用空间更大, 左边可以放map, bean, list 和 数组. 这里你可以直接用index来获取list或者array中的某个值. 例如${list[1]} 或者 ${list["1"]}, 这两者是一样的, 最终都会转化为1, 然后寻找list中的第二个. 如果是在四个作用域内已经设置过的属性(某个bean), 实际上并不需要使用xxxScope.xxxBean.xxxProperty或者是xxxScope["xxxBean"].xxxProperty, 直接xxxBean.xxxProperty即可. 查找顺序是 page, request, session, context依次查找.
el中的隐式对象 :
- pageScope, requestScope, sessionScope, applicationScope这4个分别对应4个作用域下的保存属性的map
- param, paramValues 这是http请求参数的map, 后者用于存在多个同名参数情况.
- header, headValues 这是请求首部的map
- cookie 关于cookie的map, Servlet提供的是一个数组, 所以这里比Servlet要方便.
- initParam 这个不是Servlet的初始化参数而是context的初始化参数, 也就是整个应用的初始化参数的map.
- pageContext 这个不是map, 这个和上面scriptlet中那个一样. 可以用这个访问Servlet的初始化参数 : ${pageContext.servletConfig.getInitParameter("name")}
这里还有内容暂时不提, 还有el函数和jstl没有提, 先留个坑, 以后有时间再填...
JSP中的包含与转发
JSP中有两种包含方式 :
1. <%@ include file="the_jspFile_path"%> : 这种方式的包含实际上发生在jsp被转换成java代码之前, 也就是说, 将被包含的jsp直接替换到该语句的位置, 然后将形成的新的的jsp转化为java代码, 然后编译加载并提供服务. 所以这种包含也称之为静态包含.
2. <jsp:include page="the_jspFile_path" /> : 这种方式的包含实际上不算包含, 当请求来临时, 主jsp文件和被包含jsp文件各自被转换编译加载后, 由主jsp文件形成的类运行时调用被包含jsp文件形成的类, 个人感觉可以理解为请求被包含的jsp文件形成的类在运行时帮助主jsp文件形成的类完成一部分页面的构造工作. 所以这种包含也称之为动态包含. 这种包含还可以有参数.
<jsp:include page="the_jspFile_path"><jsp:param name="param_name" value="param_value" />
</jsp:include>
对于被包含页面而言, 请求参数中增加了上述添加的这个.
处于效率考虑, 个人感觉没有特殊要求的话, 统一使用第一个比较好...
JSP标准动作中还存在一个用于转发的动作, 该动作和 :
<jsp:forward page="the_page_path" />
实际上该动作等同于
req.getRequestDispatcher("/WEB-INF/jsp/login/login.jsp").forward(req, resp);
是在服务器端进行的, 用户并不能看到url的变化.
同时该动作不需要担心写在jsp中部会导致转发之后之前加载的页面会打印出来, 因为发生这个动作时容器会清空缓冲区, 理论上只要没有使用flush()显式地将缓冲区内容打印到页面都行. **当然之前提到的jsp include动作中的增加参数语句在这里同样试用.**