JSP&Servlet学习笔记
编写与设置Servlet
第一个Servlet程序
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/hello.view")
public class HelloServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
String name = req.getParameter("name");
out.println("<html>");
out.println("<head>");
out.println("<title>Hello Servlet</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>Hello!"+name+"!</h1>");
out.println("</body>");
out.println("</html>");
out.close();
}
}
- 该范例中继承了HttpServlet,并重新定义了doGet()方法,当浏览器GET方法发送请求时,会调用该方法。
- 在doGet()方法上可以看到HttpServletRequest和HttpServletResponse两个参数,容器接收到客户端的HTTP请求后,会收集HTTP请求中的信息,并分别创建代表请求和响应的Java对象,而后在调用doGet()方法时将这两个对象当做参数传入。可以从HttpServletRequest对象中取得有关HTTP请求的相关信息,在范例中是通过HttpServletRequest的getParameter()并指定请求参数名称,来取得用户发送的请求参数值。
- 范例中的@Override是JDK5之后所提供的标注(Annotation),作用是协助检查是否正确地定义了父类中继承下来的某个方法。
- 由于HttpServletResponse对象代表对客户端的响应,因此可以通过其setContentType()设置正确的内容类型。范例中时告知服务器,返回的响应要用text/html解析,而采用的字符编码是UTF-8.接着再使用getWriter()方法取得代表响应输出的PrintWriter对象,通过PrintWriter的println()方法来对浏览器输出相应的文字信息,在范例中是输出HTML以及根据用户名说声Hello。
- 接下来要运行Servlet,要对这个Servlet做请求,同时附上请求参数。
- 启动Tomcat后,会出现内嵌于Eclipse的浏览器,将地址栏设置为:
http://localhost:8080/war_exploded/hello.view?name=xunxun
- 这样就会看到相应的画面了。
- 但是要注意,请求的URL是
/hello.view
,这是因为有一行注解:@WebServlet("/hello.view")
这表示,如果请求的是/hello.view
就会由HelloServlet来处理请求。关于Servlet的设置以及更多细节。事实上到目前为止,由于借助了IDE的辅助,有许多细节都被省略了。
在HelloServlet之后
- 现在在IDE中编写了HelloServlet,并成功运行出结果,那么这一切是怎么串起来的,IDE又代劳了哪些事情?在IDE的项目管理中看到的文件组织结构真的是应用程序上传之后的结构么?
- Web容器是Servlet、JSP唯一认得HTTP服务器,要了解Web容器会读取哪些设置?又要求什么样的文件组织结构?Web容器对于请求到来,又会如何调用Servlet?IDE很方便,但不要过分依赖IDE。
关于HttpServlet
- 注意到HelloServlet.java中import的语句区段。
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
- 如果要编译HelloServlet.java,则类路径(Classpath)中必须包括Servlet API的相关类,如果使用的是Tomcat,则这些类通常是封装在Tomcat目录的lib子目录中的servlet-api.jar。假设HelloServlet.java位于src目录下,并放置在对应包的目录中,则可以像以下这样进行编译:
javac -classpath TOMCAT/lib/servlet-api.jar -d ./classes src/chapter2/HelloServlet.java
- 注意有些部分需要修改为实际目录位置,这样编译出的class文件会出现在classes目录中,并有对应的包层级。但是如果使用前面的方法,IDE会自动设置项目的类路径。
- 再进一步思考一个问题,为什么要在继承HttpServlet之后重新定义doGet()?又为什么HTTP请求为GET时会自动调用doGet()?首先讨论范例中看到的相关API架构,如图所示。
- 首先看到Servlet接口,定义了Servlet应当有的基本行为。例如,与Servlet生命周期相关的init()、destroy()方法,提供服务时需要调用的service()方法等。
- 实现Servlet接口的类是GenericServlet类,它还实现了ServletConfig接口,将容器调用init()方法时所传入的ServletConfig实例封装起来,而service()方法直接标示为abstract而没有任何的实现。
- 在这里只需要先注意到一件事,GenericServlet并没有规范任何关于HTTP的相关方法,而是由继承他的HttpServlet来定义。在最初定义Servlet时,并不限定他只能用于HTTP,所以没有将HTTP相关服务流程定义在GeneticServlet中,而是定义在HttpServlet的service()方法中。
可以注意包的设计,与Servlet定义相关的类或接口都位于javax.servlet包中,如Servlet、GenericServlet、ServletRequest、ServletResponse等。而与HTTP定义相关的类或接口都位于javax.servlet.http包中,如HttpServlet、HttpServletRequest、HttpServletResponse等。
- HttpServlet的service()方法流程大致如下:
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String method = req.getMethod(); //取得请求的方法
if(method.equals(METHOD_GET))
{
...;
doGet(req, resp)
...;
}
else if(method.equals(METHOD_HEAD))
{
...;
doHead(req, resp);
...;
}
else if(method.equals(METHOD_POST))
{
...;
doPost(req, resp);
...;
}
else if(method.equals(METHOD_PUT))
...;
}
- 当请求来到时,容器会调用Servlet的service()方法。 可以看到,HttpServlet的service()中定义的,基本上就是判断HTTP请求的方式,在分别调用doGet()、doPost()等方法,所以若想针对GET、POST等方法进行处理,才会只需要在继承HttpServlet之后,重新定义相对应的doGet()、doPost()方法。
使用@WebServlet
- 编写好Servlet之后,接下来要告诉Web容器有关于这个Servlet的一些信息。例如在HelloServlet.java中:
@WebServlet("/hello.view")
public class HelloServlet extends HttpServlet{
- 只要在Servlet上设置@WebServlet标注,容器就会自动读取当中的信息。上面的@WebServlet告诉容器,如果请求的URL是“/hello.view”,则由HelloServlet的实例提供服务。可以使用@WebServlet提供更多信息:
@WebServlet(
name = "Hello",
urlPattens={
"/hello.view"},
loadOnStartup=1
)
- 上面的@WebServlet告知容器,HelloServlet这个Servlet的名称是Hello,这是由name属性指定的,而如果客户端请求的URL是/hello.view,则由具Hello名称的Servlet来处理,这是由urlPatterns属性来指定的。在Java EE相关应用程序中使用标注时,可以记得的是,没有设置的属性通常会有默认值。例如,若没有设置@WebServlet的name属性,默认值会是Servlet的类完整名称。
- 当应用程序启动时,事实上并没有创建所有的Servlet实例。容器会在首次请求需要某个Servlet服务时,才将对应的Servlet类实例化、进行初始化操作,然后在处理请求。这意味着第一次请求该Servlet客户端,必须等待Servlet类实例化、进行初始动作所必须花费的时间,才真正得到请求的处理。
- 如果希望应用程序启动时,就先将Servlet类载入、实例化并做好初始化动作,则可以使用loadOnStartup设置。设置大于零的值(默认-1),表示启动应用程序后就要初始化Servlet。数字代表了Servlet的初始顺序,容器必须保证有较小数字的Servlet现进行初始化,在有多个Servlet使用了相同的数字时,则容器实现厂商可以自行决定要如何载入哪个Servlet。
使用web.xml
- 使用标注来定义Servlet是Java EE6中Servlet3.0之后才有的功能,在先前的版本中,必须在Web应用程序的WEB-INF目录中,建立一个web.xml文件来定义Servlet相关信息。在Servlet3.0中,也可以使用web.xml文件来定义Servlet
- 例如:可以在先前的FirstServlet项目中找到web.xml并以添加一些内容:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>chapter2.HelloServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/helloUser.view</url-pattern>
</servlet-mapping>
</web-app>
- 这样的文件称为部署描述文件。使用web.xml定义是比较麻烦一些,不过其中的设置会覆盖Servlet中的标注设置,可以使用标注来做默认值,而web.xml来作为更改设置值之用。在上例中,如果有客户端请求
/helloUser.view
,则由HelloServlet这个Servlet来处理,这跟别是由<url-pattern>和<servlet-name>来定义,而HelloServlet名称的Servlet实际上是chapter2.HelloServlet的实例,这分别是由<servlet>中的<servlet-name>和<servlet-class>来定义。如果有多个Servlet在设置<load-on-startup>时使用了相同的数字,则按照在web.xml中设置的顺序来初始Servlet。 - 无论使用哪种方式,我们发现,请求时的URL是个逻辑名称,请求
/hello.view
并不是指服务器上真有一个实体文件叫hello.view,而回再由Web容器对应至实际处理请求的文件或程序实体名称。如果愿意,也可以再用一个像hello.jsp之类的名称伪装资源。 - 到目前为止,我们知道,一个Servlet在web.xml中有三个名称设置,<url-pattern>设置逻辑名称,<servlet-name>注册Servlet名称,以及<servlet-class>设置实体类名称。
文件组织与部署
- IDE为了管理项目资源,会有其项目专属的文件组织,但那并不是真正上传至Web容器之后该有的架构。Web容器要求应用程序部署时,必须遵照固定的结构。
进阶部署设置
- 初学Servlet/JSP,了解本章之前所说明的目录结构与部署设置已经足够,然而在Servlet3.0中,增加了一些新的部署设置方式,可以让Servlet的部署更方便、更模块化、更具弹性。
URL模式设置
- 一个请求的URI实际上是由三个部分组成的:
requestURI = contextPath + servletPath + pathInfo
环境路径
- 可以使用HttpServletRequest的getRequestURI()来取得这项信息,其中contextPath时环境路径,是容器用来决定该挑选哪个Web应用程序的依据,环境路径设置方式标准中并没有规范,按照使用的应用程序服务器而有所不同。
- 可使用HttpServletRequest的getContextPath()来取得环境路径。如果应用程序环境路径与Web服务器环境根路径相同,则应用程序环境路径为空字符串,如果不是,则应用程序环境路径以“/”开头,但不包括“/"结尾。
- 一旦决定是哪个Web应用程序来处理请求,接下来就进行Servlet的挑选,Servlet必须设置URL模式。可以设置的格式分别说明如下:
- 路径映射:以
/
开头且/*
结尾,例如/guest/*
,则请求URI扣去环境路径的部分若为/guest/test.view
等以/guest/
作为开头的,都会交由该Servlet处理。 - 扩展映射:以
*.
开头的URL模式,例如*.view
则所有以.view
结尾的请求都会交由该Servlet处理。 - 环境根目录映射:空字符串""是个特殊的URL模式,对应至环境根目录,也就是
/
的请求,但不用于设置<url-pattern>或urlPattern属性。 - 预设Servlet:仅包括
/
的URL模式,当找不到适合的URL模式对应时就会使用预设Servlet - 完全匹配:不符合以上设置的其他字符串,都要做路径的严格对应。
- 路径映射:以
Servlet路径
- Servlet路径直接对应至URL模式信息,可使用HttpServletRequest的getServletPath()来取得,Servlet路径基本上是以
/
开头,但/*
与"“的URL模式比对而来的请求除外,在/*
与”"的情况下,getServletPath()取得的Servlet路径就是空字符串。
路径信息
- 在最上面的requestURI中,pathInfo的部分是指路径信息(Path info),路径信息不包括请求参数,指的是不包括环境路径与Servlet路径部分的额外路径信息。可使用HttpServletRequest的getPathInfo()来取得。如果没有额外路径信息,则为null(扩展映射、预设Servlet、完全匹配),如果有额外路径信息,贼是一个以
/
开头的字符串。 - 编写以下Servlet:
package chapter2;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/servlet/*")
public class PathServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Servlet Pattern</title>");
out.println("</head>");
out.println("<body>");
out.println(req.getRequestURI() + "<br>");
out.println(req.getContextPath() + "<br>");
out.println(req.getServletPath() + "<br>");
out.println(req.getPathInfo());
out.println("</body>");
out.println("</html>");
out.close();
}
}
- 如果在浏览器中输入URL为
http://localhost:8080/war_exploded/servlet/path.view
- 网页上的输出结果为:
/war_exploded/servlet/path.view
/war_exploded
/servlet
/path.view
Web目录结构
- 在第一个Servlet中简单介绍过Web应用程序目录结构,这里再做个详细说明,一个Web应用程序基本上会由以下项目组成:
- 静态资源(HTML、图片、声音等)
- Servlet
- JSP
- 自定义类
- 工具类
- 部署描述文件(web.xml等)、设置信息(Annotation等)
- Web应用程序目录结构必须符合规范。举例来说,如果一个应用程序的环境路径(Context path)是/openhome,则所有的资源项目必须以/openhome为根目录依规定结构摆放。基本上根目录中的资源可以直接下载,例如若index.html位于/openhome下,则可以直接以/openhome/index.html来取得。
- Web应用程序存在一个特殊的/WEB_INF目录,此目录中存在的资源项目不会被列入应用程序根目录中可直接访问的项。也就是说,客户端(例如浏览器)不可以直接请求/WEB-INF中的资源,否则就是404 Not Found的错误结果。
- Web应用程序用到的JAR文件,其中可以放置Servlet、JSP、自定义类、工具类、部署描述文件等,应用程序的类载入器可以从JAR中载入对应的资源。
- 可以在JAR文件的
/META-INF/resources
目录中放置静态资源或JSP等,例如若在/META-INF/resources
中放个index.html,若请求的URL中包括/openhome/index.html
但实际上/openhome
根目录下不存在index.html,则会使用JAR中的/META-INF/resources/index.html
- 如果要用到某个类,则Web应用程序回到
/WEB-INF/classes
中尝试载入类,若无,则再试着从/WEB-INF/lib
中的JAR文件中寻找类文件,若还没有找到,则会到容器实现本身存放类或JAR的目录中寻找,但位置实现厂商而有所不同,以Tomcat而言,搜寻路径是Tomcat安装目录下的lib目录。 - 客户端不可以直接请求
/WEB_INF
中的资源,但可以通过程序的控制,让程序来取得/WEB-INF
中的资源,如使用ServletContext的getResource()与getResourceAsStream()或是通过RequestDispatcher请求调派。 - 如果Web应用程序的URL最后是以
/
结尾,且确实存在该目录,则Web容器必须传回该目录下的欢迎界面,可以在部署描述文件web.xml中包括以下的定义,支出可用的欢迎界面名称为何。Web容器会依序看看是否有对应文件存在,如果有,则返回给客户端。
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
- 如果找不到以上的文件,则会尝试至JAR的
/META-INF/resources
中寻找已放置的组员界面。如果URL最后是以/
结尾,但不存在该目录,则会使用预设Servlet。 - 整个Web应用程序可以被封装为一个WAR文件,如openhome.war,以便部署至Web容器。
使用web-fragment.xml
- Servlet3.0中,可以使用标注来设置Servlet的相关信息。实际上,Web容器不仅读取
/WEB-INF/classes
中的Servlet标注信息,如果一个JAR文件中有使用标注的Servlet,Web容器也可以读取标注信息、载入类并注册为Servlet进行服务。 - 在Servlet3.0中,JAR文件可用来作为Web应用程序的部分模块。事实上,不仅是Servlet,监听器、过滤器等也可以在编写、定义标注完毕后,封装在JAR文件中,视需要放置在Web应用程序的
/WEB-INF/lib
中,弹性抽换Web应用程序的功能性。
web-fargment.xml
- 一个JAR文件中,除了可使用标注定义的Servlet、监听器、过滤器外,也可以拥有自己的部署描述文件,这个文件的名称是web-fragment.xml,必须放置在JAR文件的META-INF目录中。基本上,web.xml中可定义的元素,在web-fragment.xml中也可以定义。举个例子来说,可以在web.fragment.xml中定义如下内容:
<?xml version="1.0" encoding="UTF-8" ?>
<web-fragment xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-fragment_3_1.xsd"
version="3.1">
<name>WebFragment1</name>
<servlet>
<servlet-name>hi</servlet-name>
<servlet-class>chapter2.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hi</servlet-name>
<url-pattern>/hi.view</url-pattern>
</servlet-mapping>
</web-fragment>