Java Web 高级编程 - 第三章 创建第一个Servlet
本章内容
javax.servlet.http.HttpServlet
目前Java 7 EE支持的唯一Servlet协议就是HTTP。
HttpServletRequest;HttpServletResponse
import javax.servlet.http.HttpServlet; public class HelloServlet extends HttpServlet { }
通过以上的方式,该Servlet已经可以接受任何HTTP请求,并返回一个405 Method Not Allowed错误。
任何未重写HTTP Servlet方法都将返回一个HTTP状态405作为响应。
使用初始化方法和销毁方法
当Web容器启动Servlet时,将会调用Servlet的init方法。有时但不总是,在部署应用程序时也会调用该方法。
稍后再Web容器关闭Servlet时,它将调用Servlet的destroy方法。
@Override public void init() throws ServletException { System.out.println("Servlet " + this.getServletName() + " has started."); } @Override public void destroy() { System.out.println("Servlet " + this.getServletName() + " has stopped."); }
init方法在Servlet构造完成之后调用,但在响应第一个请求之前。与构造器不同,在调用init方法时,Servlet中的所有属性都已经设置完成,并提供对ServletConfig和ServletContext对象的访问。
如果将Servlet配置为在Web应用程序部署和启动时自动启动,那么它的init方法也将会被调用。否则init方法将在第一次请求访问它接收的Serv时调用。
同样地,destroy在Servlet不再接收请求之后立即调用。这通常发生在Web应用程序被停止或卸载,或者Web容器关闭时。
因为它将在卸载或关闭时立即调用,所以不需要等待垃圾回收在清理资源之前触发终止化器。
向描述符中添加Servlet
<?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_3_1.xsd" version="3.1"> <display-name>Hello World Application</display-name> </web-app>
上面的示例中,标记的代码表示应用程序在应用服务器中显示的名字。Tomcat管理器页面中将显示出<display-name>标签中配置的名字。
标签<web-app>中的version特性表示应用程序使用的是哪一个Servlet API版本。
添加Servlet标签:
<?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_3_1.xsd" version="3.1"> <display-name>Hello World Application</display-name> <servlet> <servlet-name>helloServlet</servlet-name> <servlet-class>com.wrox.HelloServlet</servlet-class> </servlet> </web-app>
可以对Servlet配置做简单的调整,使它在Web应用程序启动之后立即启动:
<servlet> <servlet-name>helloServlet</servlet-name> <servlet-class>com.wrox.HelloServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet>
如果多个Servlet配置都包含了该标签,它们将按照标签内的值得大小顺序启动,上面代码中使用的1表示第一个启动,数字越大启动越晚。
如果两个或多个Servlet的<load-on-startup>配置相同,那么将按照它们在描述符文件中的出现顺序启动,其他Servlet仍然按照大小顺序依次启动。
将Servlet映射到URL
在告诉应用服务器如何启动Servlet之后,接着需要告诉该Servlet应该对那些请求URL作出响应。
<?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_3_1.xsd" version="3.1"> <display-name>Hello World Application</display-name> <servlet> <servlet-name>helloServlet</servlet-name> <servlet-class>com.wrox.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>helloServlet</servlet-name> <url-pattern>/greeting</url-pattern> </servlet-mapping> </web-app>
使用了以上配置后,所有访问应用程序相对URL/greeting的请求都将有helloServlet处理。
<servlet>和<servlet-mapping>标签内的<servlet-name>标签应该一致。
也可以将多个URL映射到相同的Servlet:
<servlet-mapping> <servlet-name>helloServlet</servlet-name> <url-pattern>/greeting</url-pattern> <url-pattern>/salutation</url-pattern> <url-pattern>/wazzup</url-pattern> </servlet-mapping>
在以上配置中,三个URL都将被映射到相同的Servlet:helloServlet。
那么为什么要先创建一个Servlet实例,然后再通过名字将URL映射到该Servlet呢?为什么不直接将URL映射到Servlet类呢?
如果在一个在线商店应用程序中,有两个不同仓库的Servlet。它们可能有着相同的逻辑,但是链接到不同的数据库。那么可以通过以下的方式实现:
<servlet> <servlet-name>oddsStore</servlet-name> <servlet-class>com.wrox.StroeServlet</servlet-class> </servlet> <servlet> <servlet-name>endsStore</servlet-name> <servlet-class>com.wrox.StroeServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>oddsStore</servlet-name> <url-pattern>/odds</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>endssStore</servlet-name> <url-pattern>/ends</url-pattern> </servlet-mapping>
现在创建了两个实例,它们都使用相同的Servlet类,但是名字不同,并且被映射到了不同的URL上。那么这两个示例如何知道自己使用的是哪个仓库呢?
在Servlet代码的任何位置调用getServletName即可区分出这两个实例。
运行和调试Servlet
HttpServletRequest接口是对ServletRequest的扩展,它将提供关于收到请求的额外的与HTTP协议相关的信息。
它指定了多个可以获得HTTP请求的详细信息的方法。它也允许设置请求特性。
1.获取请求参数
无论参数是作为查询参数(get)还是post变量传入,都可以调用请求对象中与参数相关的方法来获取它们。
getParameter将返回参数的单个值。如果参数有多个值,getParameter将返回第一个值。
getParameterValues将返回参数的值得数组。如果参数只有一个值,该方法将返回只有一个元素的数组。
getParameterMap将返回一个包含了所有参数名值对的Map<String,String[]>。
getParameterNames将返回所有可用参数的名字的枚举。
2.确定与请求内容相关的信息
getContentType返回请求的MIME内容类型。
getContentLength;getContentLengthLong都返回了请求正文的长度,以字节为单位。getContentLengthLong用于那些内容长度超过2GB的请求。
当请求中包含字符类型的内容时,getCharacterEncoding返回请求内容的字符编码。
3.读取请求的内容
getInputStream返回ServletInputStream。
getInputStream;getReader都可以用于读取请求的内容。
使用哪种方法取决于上下文-所需读取的请求的内容类型。
如果请求是基于字符编码的,那么使用BufferedReader通常是最简单的方式,因为它可以帮助你轻松地读取字符数据。
如果请求数据是二进制格式的,那么就必须使用ServletInputStream,这样才可以访问字节格式的请求内容。
永远不要在同一个请求上同时使用getReader和getInputStream。在调用其中一个方法后,再调用另一个将会触发IllegalStateException异常。
任何时候在使用含有post变量的请求时,最好只使用参数方法,不要使用getReader和getInputStream方法。
4.获取请求特有的数据,例如URL、URI和头
getRequestURL:返回客户端用于创建请求的完整URL。
Reconstructs the URL the client used to make the request. The returned URL contains a protocol, server name, port number, and server path, but it does not include query string parameters.
getRequestURI:与getRequestURL稍有不同,它只返回URL中的服务器路径部分。
| First line of HTTP request | Returned Value |
|---|---|
| POST /some/path.html HTTP/1.1 | /some/path.html |
| GET http://foo.bar/a.html HTTP/1.0 | /a.html |
| HEAD /xyz?a=b HTTP/1.1 | /xyz |
getServletPath:类似于getRequestURI,它将返回更少的URL。如果请求访问的时/hello-world/greeting?foo=world,Servlet映射为/greeting。getServletPath将只返回用于匹配Servlet映射的URL部分:/greeting。
getHeader(String name):返回指定名称的头数据。
getHeaderNames:返回请求中所有头数据的名字和枚举。
getIntHeader(String name):如果有某个特定的头的值一直是数字,那么可以调用该方法返回一个数字。NumberFormatException - If the header value can't be converted to an int。
getDateHeader(String name):对于可以表示有效时间戳的头数据,该方法将返回一个Unix时间戳(毫秒)。IllegalArgumentException - If the header value can't be converted to a date
5.会话和Cookies
HttpServletResponse接口继承了ServletResponse,所以HttpServletResponse也提供了对响应中与HTTP协议相关属性的访问。
可以使用响应对象完成设置响应头、编写响应正文、重定向请求、设置HTTP状态码以及将Cookies返回到客户端等任务。
1.编写响应正文
getOutputStream将返回一个ServletOutputStream。
getWriter将返回一个PrintWriter。
使用PrintWriter可以轻松地向响应中输出编码字符串和字符。但是如果要返回二进制数据,就必须使用getOutputStream。
同样地,如果两种方法被同一个响应对象同时使用,也会触发IllegalStateException。
2.设置头和其他响应属性
setHeader;setIntHeader;setDateHeader;addDateHeader;addHeader;addIntHeader
getHeader;getHeaderNames;getHeaders;containsHeader
getStatus;setStatus;sendError;sendRedirect
doGet
private static final String DEFAULT_USER = "Guest"; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //检测请求中是否包含user参数,如果未包含,就使用DEFAULT_USER常量。 String user = request.getParameter("user"); if(user == null) user = HelloServlet.DEFAULT_USER; //将响应的内容设置为text/html,并将字符编码设置为UTF-8 response.setContentType("text/html"); response.setCharacterEncoding("UTF-8"); //从响应中获得一个PrintWriter,并输出一个兼容与HTML5的文档 PrintWriter writer = response.getWriter(); writer.append("<!DOCTYPE html>\r\n") .append("<html>\r\n") .append("<head>\r\n") .append("<title>Hello User Application</title>\r\n") .append("</head>\r\n") .append("<body>\r\n") .append("Hello, ").append(user).append("!<br/><br/>\r\n") .append("<form action=\"greeting\" method=\"POST\">\r\n") .append("Enter your name:<br/>\r\n") .append("<input type=\"text\" name=\"user\"/><br/>\r\n") .append("<input type=\"submit\" value=\"Submit\"/>\r\n") .append("</form>\r\n") .append("</body>\r\n") .append("</html>\r\n"); }
doPost
@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //如果表单的方法设置为POST时,简单的调用doGet实现对请求的处理 this.doGet(request, response); }
最后一处需要注意的是Servlet声明上的注解:
@WebServlet( name = "helloServlet", urlPatterns = {"/greeting", "/salutation", "/wazzup"}, loadOnStartup = 1 ) public class HelloServlet extends HttpServlet { }
注解可以用来代替之前web.xml文件中编写的描述符:
<servlet> <servlet-name>helloServlet</servlet-name> <servlet-class>com.wrox.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>helloServlet</servlet-name> <url-pattern>/greeting</url-pattern> <url-pattern>/salutation</url-pattern> <url-pattern>/wazzup</url-pattern> </servlet-mapping>
多值参数的例子:
Servlet中的doGet方法将输出一个含有5个复选框的简单表单。用户选择任意数目的复选框并单击Submit(将由doPost方法处理)。
该方法将获得所有的水果值并在页面中使用无序列表显示出来。
@WebServlet( name = "multiValueParameterServlet", urlPatterns = {"/checkboxes"} ) public class MultiValueParameterServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); response.setCharacterEncoding("UTF-8"); PrintWriter writer = response.getWriter(); writer.append("<!DOCTYPE html>\r\n") .append("<html>\r\n") .append(" <head>\r\n") .append(" <title>Hello User Application</title>\r\n") .append(" </head>\r\n") .append(" <body>\r\n") .append(" <form action=\"checkboxes\" method=\"POST\">\r\n") .append("Select the fruits you like to eat:<br/>\r\n") .append("<input type=\"checkbox\" name=\"fruit\" value=\"Banana\"/>") .append(" Banana<br/>\r\n") .append("<input type=\"checkbox\" name=\"fruit\" value=\"Apple\"/>") .append(" Apple<br/>\r\n") .append("<input type=\"checkbox\" name=\"fruit\" value=\"Orange\"/>") .append(" Orange<br/>\r\n") .append("<input type=\"checkbox\" name=\"fruit\" value=\"Guava\"/>") .append(" Guava<br/>\r\n") .append("<input type=\"checkbox\" name=\"fruit\" value=\"Kiwi\"/>") .append(" Kiwi<br/>\r\n") .append("<input type=\"submit\" value=\"Submit\"/>\r\n") .append(" </form>") .append(" </body>\r\n") .append("</html>\r\n"); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String[] fruits = request.getParameterValues("fruit"); response.setContentType("text/html"); response.setCharacterEncoding("UTF-8"); PrintWriter writer = response.getWriter(); writer.append("<!DOCTYPE html>\r\n") .append("<html>\r\n") .append(" <head>\r\n") .append(" <title>Hello User Application</title>\r\n") .append(" </head>\r\n") .append(" <body>\r\n") .append(" <h2>Your Selections</h2>\r\n"); if(fruits == null) writer.append(" You did not select any fruits.\r\n"); else { writer.append(" <ul>\r\n"); for(String fruit : fruits) { writer.append(" <li>").append(fruit).append("</li>\r\n"); } writer.append(" </ul>\r\n"); } writer.append(" </body>\r\n") .append("</html>\r\n"); } }
1.使用上下文初始化参数
在web.xml文件中使用<context-param>标签声明上下文初始化参数。
<context-param> <param-name>settingOne</param-name> <param-value>foo</param-value> </context-param> <context-param> <param-name>settingTwo</param-name> <param-value>bar</param-value> </context-param>
以上代码创建了两个上下文初始化参数:值为foo的settingOne和值为bar的settingTwo。在Servlet代码的任何地方都可以轻松地获得和使用这些参数。
@WebServlet( name = "contextParameterServlet", urlPatterns = {"/contextParameters"} ) public class ContextParameterServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ServletContext c = this.getServletContext(); PrintWriter writer = response.getWriter(); writer.append("settingOne: ").append(c.getInitParameter("settingOne")) .append(", settingTwo: ").append(c.getInitParameter("settingTwo")); } }
应用程序中的所有Servlet都将共享这些初始化参数,在所有的Servlet中它们的值也都是相同的。
有时需要使某个设置只作用于某一个Servlet,那么就需要用到Servlet初始化参数。
2.使用Servlet初始化参数
以下代码基本功能与前面的ContextParameterServlet一致,但是它不是从ServletContext对象中获取初始化参数,而是从ServletConfig中。
public class ServletParameterServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ServletConfig c = this.getServletConfig(); PrintWriter writer = response.getWriter(); writer.append("database: ").append(c.getInitParameter("database")) .append(", server: ").append(c.getInitParameter("server")); } }
当然,只有Servlet代码是不够的。将下面的XML添加到部署描述符中,它将声明和映射Servlet,并完成一些额外的工作。
<servlet> <servlet-name>servletParameterServlet</servlet-name> <servlet-class>com.wrox.ServletParameterServlet</servlet-class> <init-param> <param-name>database</param-name> <param-value>CustomerSupport</param-value> </init-param> <init-param> <param-name>server</param-name> <param-value>10.0.12.5</param-value> </init-param> </servlet>
标签<init-param>如同Servlet上下文的<context-param>一样,创建了专属于该Servlet的初始化参数。
如何使用注解的方式完成Servlet初始化参数的设置呢?
@WebServlet( name = "servletParameterServlet", urlPatterns = {"/servletParameters"}, initParams = { @WebInitParam(name = "database", value = "CustomerSupport"), @WebInitParam(name - "server", value = "10.0.12.5") } ) public class ServletParameterServlet extends HttpServlet { }
不过,这样做有一个缺点,那就是在修改了Servlet初始化参数之后必须重新编译应用程序。
1.配置Servlet支持文件上传
@WebServlet( name = "ticketServlet", urlPatterns = {"/tickets"}, loadOnStartup = 1 ) @MultipartConfig( fileSizeThreshold = 5_242_880, //5MB maxFileSize = 20_971_520L, //20MB maxRequestSize = 41_943_040L //40MB ) public class TicketServlet extends HttpServlet { }
Annotation Type MultipartConfig
注解@MultipartConfig告诉Web容器这个Servlet提供文件上传支持。
location:该特性告诉浏览器应该在哪里存储临时文件,不过大多数情况下,都可以忽略该字段,让应用服务器使用它的默认临时目录即可。
fileSizeThreshold:告诉Web容器文件必须达到多大才能写入到临时目录中。
在本例中,小于5MB的上传文件将保存在内存中,知道请求完成,然后由垃圾回收器回收。对于超过5MB的文件,容器将把该文件保存在location指向的目录中,在请求完成之后,容器将从磁盘中删除该文件。
maxFileSize:设置指定了最大上传文件的大小。本例中为20MB。
maxRequestSize:则会禁止大小超过40MB的请求,不论它上传了多少个文件。
也可以使用部署描述符取代@WebServlet和@MultipartConfig。在<servlet>标签中,可以添加一个<multipart-config>标签,在该标签中则可以使用<location>;<file-size-threshold>;<max-file-size>;<max-request-size>标签。
doGet代码实现:
private volatile int TICKET_ID_SEQUENCE = 1; private Map<Integer, Ticket> ticketDatabase = new LinkedHashMap<>(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if(action == null) action = "list"; switch(action) { case "create": this.showTicketForm(response); break; case "view": this.viewTicket(request, response); break; case "download": this.downloadAttachment(request, response); break; case "list": default: this.listTickets(response); break; } }
doPost代码实现:
@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if(action == null) action = "list"; switch(action) { case "create": this.createTicket(request, response); break; case "list": default: response.sendRedirect("tickets"); break; } }
doPost方法中一个新的变化是:使用了重定向方法。如果客户端执行了一个不含action参数或者含有无效action参数的POST请求,浏览器页面将会被重定向至显示票据的页面。
2.接收文件上传
最后查看方法createTicket,以及他所使用的processAttachment方法。
方法processAttachment将从multipart请求中获得InputStream,并将它复制到Attachment对象中。
它使用了Servlet 3.1中新增的getSubmittedFileName方法,用于识别文件在上传之前的原始名称。
方法createTicket将使用该方法和其他请求参数填充Ticket对象,并将该对象添加到数据库中。
private void createTicket(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Ticket ticket = new Ticket(); ticket.setCustomerName(request.getParameter("customerName")); ticket.setSubject(request.getParameter("subject")); ticket.setBody(request.getParameter("body")); Part filePart = request.getPart("file1"); if(filePart != null && filePart.getSize() > 0) { Attachment attachment = this.processAttachment(filePart); if(attachment != null) ticket.addAttachment(attachment); } int id; synchronized(this) { id = this.TICKET_ID_SEQUENCE++; this.ticketDatabase.put(id, ticket); } response.sendRedirect("tickets?action=view&ticketId=" + id); } private Attachment processAttachment(Part filePart) throws IOException { InputStream inputStream = filePart.getInputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); int read; final byte[] bytes = new byte[1024]; while((read = inputStream.read(bytes)) != -1) { outputStream.write(bytes, 0, read); } Attachment attachment = new Attachment(); attachment.setName(filePart.getSubmittedFileName()); attachment.setContents(outputStream.toByteArray()); return attachment; }
理解请求、线程和方法执行
Web容器通常会包含某种类型的线程池,它们被成为连接池或执行池。
当容器收到请求时,它将在池中寻找可用的线程。如果找不到可用的线程,并且线程池已经达到了最大线程数,那么该请求将会被放入一个队列中 - 先进先出 - 等待获得可用的线程。一旦出现可用线程,浏览器将从线程池中借出线程,并将请求传递给线程,由线程进行处理。此时,该线程对于其他请求是不可用的。在普通请求中,线程和请求的关联将会贯穿请求的整个生命周期。只要请求正在由应用程序代码处理,该线程就只属于这个请求。只有在请求完成,相应内容已经发送到客户端后,该线程才会变成可用状态并返回到线程池中,用于处理下一个请求。
创建和销毁线程会产生许多开销,这可能会降低应用程序的运行速度,所以采用由可复用线程组成的线程池可以减少这种开销,提高性能。
线程池有一个可以配置的大小属性,通过它可以决定一次可以创建多少连接。
Tomcat中的最大线程池大小默认为200,可以增加或减少这个数目。必须明确这一点,因为它意味着在最糟糕的情况下,200个不同的线程可能同时在同一个实例上执行着相同的方法。因此,必须考虑代码的运行方式,避免代码在多个线程中并发执行的情况下出现异常行为。
保护共享资源
在编写多线程应用程序是最典型的问题就是对共享资源的访问。方法中创建的对象和变量在方法执行过程中都是安全的 - 其他线程无法访问它们。
不过,Servlet中的静态变量和实例变量都可以被多个线程同时访问。对这些共享资源进行同步是非常重要的,只有这样才能避免损坏资源的内容,也能比曼可能由应用程序引起的错误。
有多种技术可以用于保护共享资源:
private volatile int TICKET_ID_SEQUENCE = 1;
synchronized(this) { id = this.TICKET_ID_SEQUENCE++; this.ticketDatabase.put(id, ticket); }
Java 理论与实践: 正确使用 Volatile 变量
Java™ 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。其中 Volatile 变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错。与锁相比,Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件 —— 即变量真正独立于其他变量和自己以前的值 —— 在某些情况下可以使用 volatile 代替 synchronized 来简化代码。然而,使用 volatile 的代码往往比使用锁的代码更加容易出错。
| Chapter 3 Updated on 9/4/14. |
104.17 KB | Click to Download |
摘录自:[美]Nicholas S.Williams著,王肖峰译 Java Web高级编程 [M]、清华大学出版社,2015、35-62、

浙公网安备 33010602011771号