1. 工程创建步骤:
1) Ctrl + N创建工程,选择Web -> Dynamic Web Project;
2) 输入工程名并确认Web容器后Finish创建完毕;
3) 在左侧Project Explorer视图中展开工程目录 -> Java Resources,在src上右键New -> Servlet;
4) 输入类名、包名后Next;
5) Edit URL mappings,将Pattern改为/hello.view;
6) Finish创建完毕;
7) 添加代码如下:
package com.lirx; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class HelloServlet */ @WebServlet("/hello.view") public class HelloServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public HelloServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse * response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); String name = request.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(); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse * response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }8) 代码编写完之后Run As Server(Alt + Shift + X, R),会自动打开一个内嵌于Eclipse的浏览器页面出现服务器返回的结果,你需要在浏览器的地址栏中加上请求参数?name=Lirx,则会返回字符串"Hello!Lirx!";
!!注意:Tomcat的默认端口号是8080,要确保该端口没有被其它应用程序占用;
9) 可以查看返回的网页代码,发现内容是:
<html> <head> <title>Hello Servlet</title> </head> <body> <h1> Hello! Lirx !</h1> </body> </html>
2. 程序中的重要元素:
1) 首先是@WebServlet(写在Servlet类定义之前),可以看到URL中的路径(/hello.view)就是@WebServlet中指定的路径,而实际上该Servlet并没有存放在该路径下,并且该路径也不存在!!
!这其实是Servlet的部署功能,该功能的作用就是,如果URL上的路径是/hello.view,那么Web容器就会让HelloServlet来处理该请求,而/hello.view这个不存在的虚拟路径隐藏了Servlet的真正路径!!
!!这样做的好处有两个:
a. 一是安全考虑,不必让用户知道真正的程序放在什么路径下;
b. 二是见名知意,我可以取一个非常有意义的路径名让用户一目了然自己访问的是什么内容,比如上面的hello.view就知道是一个显示输出的文件(.view就是视图的意思);
2) Servlet类继承自HttpServlet类,HttpServlet类是响应Http请求的Servlet类的模板基类,里面有一些基本的接口供应用程序实现,其中最重要的就是doGet和doPost,这两个函数就会在浏览器请求GET和POST时响应;
3) HttpServletRequest类和HttpServletResponse类:
i. 这两个类对象会在Web容器接受浏览器请求时创建;
ii. 其中Request类会解析并保存请求中的信息,Servlet可以通过Request的getParameter方法获取指定请求参数的值:String getParameter(String arg0);
iii. Web容器会将这两个对象发送给doGet和doPost使用;
4) Response类:
i. 其本质是一个输出流,Servlet可以将要返回给用户的信息同PrintWriter的print等方法插入到输出流中返回给用户,而这些信息将会被浏览器当成标记语言解释(HTML、JavaScript);
ii. setContent:String setContent(String arg0); // 需要在arg0中指定击中重要的参数,其中一个是文本的解释方法,格式是"text/解释方法",这里"text/html"就是指用超文本标记语言来解释返回的文本,第二个参数是字符集,格式是“charset=字符集”,这里表示返回的内容以UTF-8进行编码;
!!注意,这些内容是返回给客户端的浏览器看的,浏览器看到这些内容时就知道该如何处理返回的文本了
iii. 接着使用Servlet指定的输出类PrintWriter的对象将要输出的内容插入到HttpServletResponse输出流中,PrintWriter有很多输出方法,println等;
!!要获取HttpServletResponse的输出对象,必须调用其方法getWriter才行!PrintWriter getWriter();
3. Servlet如何决定用哪个方法响应请求:
1) 其实在HttpServlet中定义了抽象方法service;
2) service方法也接受HttpServletRequest和HttpServletResponse两个对象;
3) 该函数中使用request的getMethod()方法(String getMethod();)获取请求的方式,请求方式用枚举来定义字符串,如METHOD_GET、METHOD_POST等;
4) 接着用If-else if-else判断请求的类型并调用相关函数响应:
String method = req.getMethod(); if (method.equals(METHOD_GET) { doGet(req, resp); ...!!service函数不要轻易覆盖!!
4. Eclipse目录配置和编译路径配置:
1) 可以看到工程目录(如何部署)以及编译运行都是Eclipse代劳的,所以我们这里了解一下细节;
!!所有的内容都会保存在以工程名命名的目录下;
2) 首先源代码目录、包目录、类目录都是自动生成的:
i. 所有用户定义的源代码包都会放在src目录下:比如我们的HelloServlet.java的包定义为com.lirx,则该源代码的路径就是src/com/lirx/HelloServlet.java;
ii. 所有编译生成的.class类文件都会自动保存在相应的包中,类文件包则会放在build/classes目录下,比如这里就是:build/classes/com/lirx/HelloServlet.class;
3) classpath类库路径:
i. 你在源代码中import的所有类在编译时都需要指定它们所在的路径,否则编译会报错,提示import的类不知道在哪里;
!!首先了解一下import如何导入:
a. 比如你要导入HttpServlet类,其磁盘路径是"Tomcat安装主目录/bin/javax/servlet/http/HttpServlet.class",
b. 那么你就可以先将"Tomcat安装主目录/bin“这个路径添加到Eclipse的classpath参数中,classpath是javac编译器的库文件搜索路径,在源文件中的各种import都会到classpath中找存不存在,如果存在则可以按照import中给定的包路径找到相应的类文件链接给它使用;
c. 在import javax.servlet.http.HttpServlet时编译器首先先将包路径转化成文件路径javax/servlet/http/HttpServlet.class,然后找该路径存不存在与classpath中的某个路径下,如果存在则成功地从磁盘相应位置加载该.class类并链接给调用它的.class文件;
d. 需要记录在classpath中的路径可以是普通的文件路径,也可以是.jar文件的路径,.jar其实是一种.zip压缩文件,.jar中压缩着各种.class和.class包,javac可以自动解压缩.jar文件并从中提取目标.class文件链接到调用它的.class文件中;
ii. 在这里用到的最基本的一些类库:JRE运行时类库,Tomcat运行时库,因此需要将JRE安装目录、Tomcat的bin目录加入到classpath中;
iii. 可以在左侧工程目录上右键选择Build Path -> Configure Build Path... -> Lirbrary -> Add External JARs,选择需要导入的外部库,导入后方可在源文件中Import这些库中的类;
!!import的导入规则:
a. import主要会从两个地方导入,一个是Eclipse中指定的外部库;
b. 其次还会从当前工程的build/classes中导入用户工程中编写的类库,因此!!!在classpath中还必须加上build/classes路径;
!!因此Eclipse隐藏的javac编译命令是:
a. 首先Eclipse的工作路径是当前工程目录,在这里就是FirstServlet目录;
b. 编译命令则是:javac -classpath ./build/classes JRE类库目录 Tomcat类库目录 其它要用到的类库目录 -d ./classes ./src/com/lirx/HelloServlet.java
5. Servlet类和HttpServlet之间的关系:
1) HttpServlet继承自Servlet,目前Servlet技术也都是应用于Http服务,那还要Servlet类有什么意义呢?
2) 其实Servlet当初设计的时候期待它不仅仅用于HTTP,这可以从Servlet的service函数中看出,Servlet的service中两个参数是ServletRequest和ServletResponse而非HttpServletRequest和HttpServletResponse;
3) HttpServlet类继承Servlet后覆盖了Servlet的service,但是该函数的参数仍然是ServletRequest和ServletResponse,可以看一下它的源码:
@Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request; HttpServletResponse response; try { request = (HttpServletRequest) req; response = (HttpServletResponse) res; } catch (ClassCastException e) { throw new ServletException("non-HTTP request or response"); } service(request, response); }!可以看到在该函数创建了两个Http的Request和Response并将Servlet的版本转换成了Http的版本(当然是类型安全的转换,向下兼容嘛,把笼统的报头信息分解成更加详细的Http报头信息保存在Http的Request和Response中);
!!最后调用的service是HttpServlet类自己定义的一个新的函数,当然是和继承来的service之间是重载关系,只不过它的参数是HttpServletRequest和HttpServletResponse;
4) 看一下HttpServlet自己定义的service的实现:
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); if (method.equals(METHOD_GET)) { long lastModified = getLastModified(req); if (lastModified == -1) { // servlet doesn't support if-modified-since, no reason // to go through further expensive logic doGet(req, resp); } else { long ifModifiedSince; try { ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE); } catch (IllegalArgumentException iae) { // Invalid date header - proceed as if none was set ifModifiedSince = -1; } if (ifModifiedSince < (lastModified / 1000 * 1000)) { // If the servlet mod time is later, call doGet() // Round down to the nearest second for a proper compare // A ifModifiedSince of -1 will always be less maybeSetLastModified(resp, lastModified); doGet(req, resp); } else { resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } } } else if (method.equals(METHOD_HEAD)) { long lastModified = getLastModified(req); maybeSetLastModified(resp, lastModified); doHead(req, resp); } else if (method.equals(METHOD_POST)) { doPost(req, resp); } else if (method.equals(METHOD_PUT)) { doPut(req, resp); } else if (method.equals(METHOD_DELETE)) { doDelete(req, resp); } else if (method.equals(METHOD_OPTIONS)) { doOptions(req,resp); } else if (method.equals(METHOD_TRACE)) { doTrace(req,resp); } else { // // Note that this means NO servlet supports whatever // method was requested, anywhere on this server. // String errMsg = lStrings.getString("http.method_not_implemented"); Object[] errArgs = new Object[1]; errArgs[0] = method; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg); } }!注意该函数没有@Override标注,说明这是一个HttpServlet自己定义的函数而并非覆盖的函数;
!!看以看到我们熟悉的对doGet、doPost等的调用;
5) 关于if-modified-since标头:
i. 可以看到上面代码中有一些和if/modified/since有关字样的代码,这其实是GET请求的一种特有的功能!只有GET才有哦!
ii. GET请求的标头中可以注明一个时间戳HEADER_IFMODSINCE,即if-modified-since时间戳,该戳会给出一个时间,即如果我要的这个资源的最后一次修改时间大于这个时间戳那么就给我返回新的资源(这种情况下客户端通常开启了缓存机制,即浏览器缓存中保存了上次请求的该资源),否则就什么都不用返回;
iii. 当然服务器端的Servlet可以选择是否理会该时间戳,是否理会取决于HttpServlet的getLastModified函数,其默认就返回一个-1,可以看到上面代码中如果返回的是-1就直接调用doGet,那么-1就表示不理会,还是直接启用doGet返回相应的资源,那如果你想让该时间戳起作用那么可以自己手动覆盖getLastModified函数让其准确返回一个该资源最后一次修改的时间;
iv. 可以通过HttpServletRequest的getDateHeader(HEADER_IFMODSINCE)来获取请求标头中的时间戳(见上面代码),得到后可以和getLastModified的返回值作比较决定是否调用doGet返回最新的资源;
6. 在继承HttpServlet时的一些问题和技巧:
1) 客户端会发出什么样的请求就要覆盖相应的doXXX函数,如果客户端请求了没有实现的请求响应就会返回HTTP Status 40X错误,比如405就是“该URL不支持GET请求”的错误,这里展示一下HttpServlet自己的doGet函数的实现,在继承HttpServlet后一定要实现相应的doXXX函数:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String protocol = req.getProtocol(); String msg = lStrings.getString("http.method_get_not_supported"); if (protocol.endsWith("1.1")) { resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg); } else { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg); } }2) 有时候可能会要求doGet和doPost进行相同的处理,此时可以自定义一个统一的processRequest函数,然后在doGet和doPost中都调用它,当然这个processRequest函数的参数肯定是HttpServletRequest和HttpServletResponse;