JavaWeb高级编程(三)—— 创建第一个Servlet

一、创建HelloServlet类

        Servlet是一个运行在Web服务器中的Java小程序,Servlet会接收和响应来自Web客户端的请求,使用HTTP进行通信。除非某些过滤器提前中止了客户端的请求,否则所有的请求都将会发送到某些Servlet中。

1、选择要继承的Servlet

        大多情况下,Servlet都继承至GenericServlet,GenericServlet是一个不依赖具体协议的Servlet,它包含了一个抽象的service()方法以及几个辅助方法用于日志操作和从应用、Servlet配置中获取信息。

        作为响应HTTP请求的HttpServlet,它继承了GenericServlet,并实现了只接受http请求的service()方法,然后它提供了响应每种http请求的方法的空实现。

import javax.servlet.http.HttpServlet;

public class HelloServlet extends HttpServlet {

}

2、添加对HTTP GET方法的支持

        这个Servlet已经可以接受任何HTTP请求,但它并没有重写HttpServlet中的方法,所以会返回一个HTTP状态405作为响应。现在我们重写doGet()方法,添加对HTTP GET方法的支持,如下:

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().print("Hello World");
    }
}
 3、使用初始化方法和销毁方法
    @Override
    public void init() throws ServletException {
        System.out.println(this.getServletName() + "has started");
    }

    @Override
    public void destroy() {
        System.out.println(this.getServletName() + "has stopped");
    }

        如果将Servlet配置为在Web应用程序部署和启动时自动启动,那么它的init()方法也将会调用。否则,init()方法将在第一次请求访问Servlet时调用。

        destroy()方法在Web应用程序被停止或卸载、关闭时立即调用,它不需要等待垃圾回收器进行 

二、配置可部署的Servlet

1、向部署描述符中添加Servlet并映射URL




    Archetype Created Web Application
    
        hello
        f1.chapter3.servlet.HelloServlet
        1
    
    
        hello
        /getting
    

        如果两个或多个Servlet的相同,那么将按照它们在描述符文件中的出现顺序启动,其他Servlet仍按照大小顺序进行启动。

2、运行和调试Servlet

如果tomcat没有设置为根目录的话,应该在getting前加上项目名,如:http://localhost:8080/xxx/getting

如果希望命中destroy的断点,需要在命令行关闭Tomcat,如果在IDE中关闭Tomcat,则Tomcat将在断点被命中之前断开与调试器的连接。

三、了解doGet、doPost和其他方法

1、获取请求参数

HttpServletRequest最重要的功能是从客户端发送的请求中获取参数。请求参数有两种不同的形式:查询参数(也称为URL参数)和post变量。

        //请求url为http://localhost:8080/getting?name=alex&name=zhangsan&age=25,请求类型为get
        String name = req.getParameter("name");//当参数有多个值时只返回第一个值
        System.out.println(name);//alex

        String[] names = req.getParameterValues("name");//返回参数值的数组
        System.out.println(names[0] + "==" + names[1]);//alex==zhangsan

        Map map = req.getParameterMap();//返回参数和值的key-value
        String[] s = (String[]) map.get("name");//alex==zhangsan
        System.out.println(s[0] + "==" + s[1]);

        Enumeration names1 = req.getParameterNames();//返回参数名字的枚举
        Object o1 = names1.nextElement();
        Object o2 = names1.nextElement();
        System.out.println(o1 + "==" + o2);//name==age
        resp.getWriter().print("Hello World");

2、确定与请求内容相关的信息

        //请求内容为age=8,请求类型为post,Request Body为Empty
        String contentType = req.getContentType();//返回请求的MIME内容类型(多用途互联网邮件扩展)
        System.out.println(contentType);//application/x-www-form-urlencoded; charset=GBK

        int length = req.getContentLength();//返回请求正文的长度,以字节为单位
        System.out.println(length);//5

        String encoding = req.getCharacterEncoding();//返回请求内容的字符编码
        System.out.println(encoding);//GBK
        resp.getWriter().print("Hello World");

3、读取请求的内容

        //请求内容为zhangsan,请求类型为post,Request Body为Text
        String contentType = req.getContentType();//返回请求的MIME内容类型(多用途互联网邮件扩展)
        System.out.println(contentType);//*/*; charset=UTF-8

        int length = req.getContentLength();//返回请求正文的长度,以字节为单位
        System.out.println(length);//8

        String encoding = req.getCharacterEncoding();//返回请求内容的字符编码
        System.out.println(encoding);//UTF-8

        BufferedReader reader = req.getReader();
        StringBuffer buffer = new StringBuffer();
        char[] c = new char[100];
        int len = 0;
        while ((len = reader.read(c)) != -1) {
            buffer.append(c, 0, len);
        }
        System.out.println(buffer.toString());//zhangsan

如果请求内容是字符编码的,那么使用BufferedReader是最简单的方式。

如果请求格式是二进制的,那么必须使用ServletInputStream,这样才可以访问字节格式的请求内容。

以上的两种方式永远不要在同一请求上同时使用这两种方法,否则会触发IllegalStateException异常。

4、获取请求的特有的数据,例如URL、URI、头

        //post请求
        StringBuffer requestURL = req.getRequestURL();
        System.out.println(requestURL);//http://localhost:8080/getting

        String requestURI = req.getRequestURI();
        System.out.println(requestURI);///getting

        String servletPath = req.getServletPath();
        System.out.println(servletPath);///getting

        String contenttype = req.getHeader("contenttype");
        System.out.println(contentType);//*/*; charset=UTF-8

5、使用HttpServletResponse

        String name = req.getParameter("name");
        resp.setContentType("application/json;charset=utf-8");
        //resp.setCharacterEncoding("utf-8");
        resp.getWriter().print("你好" + name);//你好alex

四、使用参数和接受表单提交

package f1.chapter3.servlet;

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;
//使用servlet注解
@WebServlet(
        name = "helloServlet",  
        urlPatterns = {"/getting"},
        loadOnStartup = 1
)
public class HelloServlet1 extends HttpServlet{
    private static final String USER_NAME="Tom";
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        if(null==username){
            username=HelloServlet1.USER_NAME;
        }
        resp.setContentType("text/html");
        resp.setCharacterEncoding("utf-8");
        PrintWriter writer = resp.getWriter();
        writer.append("")
              .append("")
              .append("测试")
              .append("")
              .append("Hello ").append(username+"
") .append("Enter your name:
") .append("
") .append("
") .append("
"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doGet(req,resp); //将请求委托给doGet方法 } }

浏览器请求结果如下:

JavaWeb高级编程(三)—— 创建第一个Servlet_第1张图片

输入参数然后点击提交可以请求post方法,然后将请求消息委托给get访求处理,并返回结果。

五、使用初始化参数配置应用程序

1、配置上下文初始化参数

package f1.chapter3.servlet;

import javax.servlet.ServletContext;
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(
        name = "contextParamServlet",
        urlPatterns = {"/contextParam"},
        loadOnStartup = 1
)
public class ContextParamServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletContext context = req.getServletContext();
        PrintWriter writer = resp.getWriter();
        writer.print(context.getInitParameter("username"));//Tom
    }
}

配置的上下文初始化参数是可以在多个servlet中共享的,所以需要需要配置部署描述符中的,如下:

    
    
        username
        Tom
    

2、配置当前servlet的初始化参数

package f1.chapter3.servlet;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
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(
        name = "servletParamServlet",
        urlPatterns = {"/servletParam"},
        initParams = {@WebInitParam(name = "password", value = "Tom123")},
        loadOnStartup = 1
)
public class ServletParamServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletConfig servletConfig = this.getServletConfig();//获取当前的Servlet配置
        PrintWriter writer = resp.getWriter();
        writer.print(servletConfig.getInitParameter("password"));
    }
}

注解配置servlet的初始化参数之后,必须重新编译应用程序。当然也可以配置在部署描述符中,只需更改xml代码并生重启应用程序即可,并不需要重新编译,如下:

    
    
        servletParamServlet
        f1.chapter3.servlet.ServletParamServlet
        
            password
            Tom123
        
    
    
        servletParamServlet
        /servletParam
    

六、通过表单上传文件

下面是演示一个简单的servlet项目,用于处理票据业务

1、创建两个pojo

Attachment是代表附件信息

package com.wrox;

public class Attachment
{
    private String name;

    private byte[] contents;

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    public byte[] getContents()
    {
        return contents;
    }

    public void setContents(byte[] contents)
    {
        this.contents = contents;
    }
}

Ticket代表票据

package f1.chapter3.pojo;

import java.util.LinkedHashMap;
import java.util.Map;

public class Ticket {
    private String customerName;
    private String subject;
    private String body;
    private Map attachments = new LinkedHashMap<>();

    public String getCustomerName() {
        return customerName;
    }

    public void setCustomerName(String customerName) {
        this.customerName = customerName;
    }

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }

    public Map getAttachments() {
        return attachments;
    }

    public void setAttachments(Map attachments) {
        this.attachments = attachments;
    }
}

2、创建TicketServlet处理票据相关的业务

package f1.chapter3.servlet;

import f1.chapter3.pojo.Attachment;
import f1.chapter3.pojo.Ticket;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.LinkedHashMap;
import java.util.Map;

@WebServlet(
        name = "ticketServlet",
        urlPatterns = {"/tickets"},
        loadOnStartup = 1
)
@MultipartConfig(//告诉web容器为servlet提供上传文件的支持
        fileSizeThreshold = 5_242_880,//5MB
        /**
         * 告诉web容器文件必须达到多大才能写入到临时目录中(这里小于5MB的将保存在内存中,请求完成后将由垃圾回收器回收,
         * 大于5MB的将保存在location指向的目录中,服务器会有一个默认的location目录,请求完成后,容器将从磁盘中删除此文件)
        */
        maxFileSize = 20_971_520L,//20MB,禁止上传文件大小超过20MB的文件
        maxRequestSize = 41_943_040L//40MB,禁止大小超过40MB的请求
)
public class TicketServlet extends HttpServlet {
    private volatile int TICKET_ID_SEQUENCE = 0;//使用volatile关键字,可以保证其他线程始终可以读取变量修改后的最终值
    private Map ticketDB = new LinkedHashMap<>();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String action = req.getParameter("action");
        if (null == action) {
            action = "list";
        }
        switch (action) {
            case "create":
                this.showTicketForm(resp);
                break;
            case "view":
                this.viewTicket(req, resp);
                break;
            case "download":
                this.downloadAttachment(req, resp);
                break;
            case "list":
            default:
                this.showTicketList(resp);
                break;
        }

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String action = req.getParameter("action");
        if (null == action) {
            action = "list";
        }
        switch (action) {
            case "create":
                this.createTicket(req, resp);
                break;
            case "list":
            default:
                resp.sendRedirect("tickets");
                break;
        }
    }

    /**
     * 下载attachment
     *
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    private void downloadAttachment(HttpServletRequest request,
                                    HttpServletResponse response)
            throws ServletException, IOException {
        String idString = request.getParameter("ticketId");
        Ticket ticket = this.getTicket(idString, response);
        if (ticket == null) {
            return;
        }

        String name = request.getParameter("attachment");
        if (name == null) {
            response.sendRedirect("tickets?action=view&ticketId=" + idString);
            return;
        }

        Attachment attachment = ticket.getAttachments().get(name);
        if (attachment == null) {
            response.sendRedirect("tickets?action=view&ticketId=" + idString);
            return;
        }

        response.setHeader("Content-Disposition",//强制浏览器询问是保存还是下载文件,而不是在线打开文件
                "attachment; filename=" + attachment.getName());
        response.setContentType("application/octet-stream");

        ServletOutputStream stream = response.getOutputStream();
        stream.write(attachment.getContents());//如果是下载大文件,则不应该将数据存在内存中,而是应该经常刷新ResponseOutputStream,这样数据才会不断的发送到浏览器

    }

    /**
     * 根据ticketID显示查找对应的ticket并显示
     *
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    private void viewTicket(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String ticketId = request.getParameter("ticketId");
        Ticket ticket = this.getTicket(ticketId, response);
        if (null == ticket) {
            return;
        }
        PrintWriter writer = this.writerHeader(response);

        writer.append("

Ticket #").append(ticketId) .append(": ").append(ticket.getSubject()).append("

\r\n"); writer.append("Customer Name - ").append(ticket.getCustomerName()) .append("

\r\n"); writer.append(ticket.getBody()).append("

\r\n"); if (ticket.getAttachments().size() > 0) { int i = 0; for (Attachment attachment : ticket.getAttachments().values()) { if (i++ > 0) { writer.append(", "); } writer.append("") .append(attachment.getName()).append(""); } writer.append("

\r\n"); } writer.append("Return to list tickets\r\n"); this.writerFooter(writer); } /** * 根据key值从tikcetDB中获取ticket * * @param idString * @param response * @return * @throws IOException */ public Ticket getTicket(String idString, HttpServletResponse response) throws IOException { if (null == idString && idString.length() == 0) { response.sendRedirect("tickets"); return null; } try { Ticket ticket = this.ticketDB.get(Integer.parseInt(idString)); if (null == ticket) { response.sendRedirect("tickets"); return null; } return ticket; } catch (Exception e) { response.sendRedirect("tickets"); return null; } } /** * 创建ticket并转发到ticket页面 * * @param request * @param response * @throws IOException * @throws ServletException */ private void createTicket(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { Ticket ticket = new Ticket(); ticket.setCustomerName(request.getParameter("customerName")); ticket.setSubject(request.getParameter("subject")); ticket.setBody(request.getParameter("body")); Part filePart = request.getPart("file1"); //如果filePart不为空,就向ticket中添加attachment if (null != filePart && filePart.getSize() > 0) { Attachment attachment = this.getAttachment(filePart); if (null != attachment) { ticket.getAttachments().put(attachment.getName(), attachment); } } int id; synchronized (this) {//这两个都是servlet的实例变量,这意味着多个线程可以同时访问他们,将这些操作添加到同步代码块中,可以保证其他线程无法同时执行这两行代码 id = this.TICKET_ID_SEQUENCE++; this.ticketDB.put(id, ticket); } //转发到ticket页面 response.sendRedirect("tickets?action=view&ticketId=" + id); } /** * 通过filePart来获取attachment * * @param filePart * @return * @throws IOException */ private Attachment getAttachment(Part filePart) throws IOException { InputStream inputStream = filePart.getInputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); int read; 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; } /** * 创建ticket时显示此表单,让客户填写相关信息 * * @param response * @throws IOException */ public void showTicketForm(HttpServletResponse response) throws IOException { PrintWriter writer = this.writerHeader(response); writer.append("

Create a Ticket

\r\n"); writer.append("
\r\n"); writer.append("\r\n"); writer.append("Your Name
\r\n"); writer.append("

\r\n"); writer.append("Subject
\r\n"); writer.append("

\r\n"); writer.append("Body
\r\n"); writer.append("

\r\n"); writer.append("Attachments
\r\n"); writer.append("

\r\n"); writer.append("\r\n"); writer.append("
\r\n"); this.writerFooter(writer); } /** * 显示ticket列表,如果没有显示提示信息 * * @param response * @throws IOException */ public void showTicketList(HttpServletResponse response) throws IOException { PrintWriter writer = this.writerHeader(response); writer.append("

Tickets

") .append("Create Ticket
");//可点击执行创建ticket操作 if (this.ticketDB.size() == 0) { writer.append("There are no tickets in the system.\r\n"); } else { for (int id : this.ticketDB.keySet()) { Ticket ticket = this.ticketDB.get(id); String idStr = Integer.toString(id); writer.append("Ticket #").append(idStr) .append(": ").append(ticket.getSubject()) .append(" (customer: ").append(ticket.getCustomerName()) .append(")
"); } } this.writerFooter(writer); } /** * html头部标签 * * @param response * @throws IOException */ private PrintWriter writerHeader(HttpServletResponse response) throws IOException { response.setContentType("text/html"); response.setCharacterEncoding("UTF-8"); PrintWriter writer = response.getWriter(); writer.append("") .append("") .append("测试") .append("") .append(""); return writer; } /** * html底部标签 * * @param writer */ private void writerFooter(PrintWriter writer) { writer.append("") .append(""); } }

七、编写多线程安全的应用程序

        Web容器通常会包括某种类型的线程池(连接池),当容器接收到请求时,它将在池中寻找可用的线程。如果找不到可用的线程,并用线程池已经达到了最大线程数,那么请求将被放入一个队列中,等待获得可用的线程。一旦出现可用线程,浏览器将从线程池中借出线程,并将请求传递给线程,让线程进行处理。只要请求正在由应用程序代码处理,该线程就只属于这个请求。只有在请求完成,响应内容已经发送到客户端后,该线程才会变成可用状态,并返回到线程池,用于处理下一个请求。

        创建和销毁线程会产生许多开销,这可能会降低应用程序的运行速度,所以使用线程池可以减少这种开销,提升性能。Tomat中最大线程数默认为200。

        方法中创建的对象和变量在方法执行的过程中都是安全的,其他线程无法访问。但servlet中静态变量和实例变量都可以同志被多个线程访问,因此需要对共享资源进行同步。



你可能感兴趣的:(JavaWeb高级编程)