Servlet实践--留言板-v1
功能介绍:
由三个jsp页面组成,在doGet中根据请求URL中的请求参数不同,跳转到不同的页面:
页面1:显示整个留言板列表
页面2:创建留言页面(包括用户、主题、内容和上传文件)
页面3:在查看单个留言的详细内容(包括提供下载附件)
在doPost中处理创建留言的逻辑
如何实现这些功能:
1.使用什么来保存用户创建的留言(数据存储):
使用一个Ticket类对象来保存用户创建的留言,包括用户名、评论主题、评论内容和附件。附件是用一个Attachment类的实例来表示,该类中包含附件名和附件内容的二进制表示。
当需要查看某个留言详细信息的时候,可以通过一个存储在内存中的哈希map(ticketDatabase)(以一个唯一标识某个留言的id为键,以Ticket类对象为值)来找到该Ticket类对象,通过该对象就可以得到详细信息。
当需要浏览整个留言列表时,可以通过遍历该ticketDatabase得到整个留言列表。
当需要下载某个留言中的附件时,可以先通过该ticketDatabase找到该Ticket类对象,而在Ticket类对象中也同样保存着一个哈希map(attachments)(以附件名为键,以Attachment类对象为值),通过该哈希map就可以找到某个特定的附件。
2.如何让servlet处理不同的请求(逻辑处理):
根据请求URL中的请求参数不同,让处理请求的doGet()和doPost()方法调用不同功能的方法实现。例如:
当不带任何请求参数的请求URL,默认其行为为浏览整个留言列表(action=list);
当携带请求参数时,若action=create并以Get方式提交请求,则执行将请求和响应转发到ticketForm.jsp页面;
若action=view并以Get方式提交请求,则先从请求参数中获得ticketId(唯一标识某个特定的Ticket对象),然后通过ticketDatabase获得该ticketId对应的Ticket对象,把该ticketId和Ticket对象保存在请求中,然后转发到viewTicket.jsp页面
当携带请求参数时,若action=download并以Get方式提交请求,则执行下载附件的相关操作:
+先获得该ticketId和Ticket对象,然后通过请求参数中的attachment获得附件名,得到Ticket对象和附件名,就可以得到Attachment对象
+通过Attachment对象,就可以得到附件内容的二进制表示(一个二进制数组)
+强制浏览器询问用户是保存还是下载文件,设置附件的内容类型是通用的二进制内容类型
+将附件内容的二进制数组写入ServletOutputStream输出流中
当携带请求参数时,若action=list并以Get方式提交请求时,将ticketDatabase保存在请求中,然后转发到listTickets.jsp页面;
当携带请求参数时,若action=createt并以Post方式提交请求时,将从表单中获取Ticket对象的相关属性(用户名、留言主题、留言内容、上传的文件),通过setXXX()方法分别设置Ticket对象的属性。其中文件上传时,需要将该文件转换中Part对象(filePart),Part对象可以表示一个上传的文件或者表单数据。然后生成一个唯一的ticketId,将附件内容通过IO读取的方式保存在Attachment对象中的二进制数组中,最后将ticketId和Ticket对象组成键值对添加到ticketDatabase中,把页面重定向到浏览单个留言详细内容的页面中
3.显示:
jsp页面处理显示操作
部署文件web.xml:
<jsp-config> <!-- jsp组属性,不同的jsp组可以设置不同的属性,若不同属性组发送匹配冲突时,遵循匹配精确优先原则 --> <jsp-property-group> <!-- 该jsp组属性将应用于哪些文件,在这里它将匹配在Web应用程序中所有以jsp和jspf文件结尾的文件--> <url-pattern>*.jsp</url-pattern> <url-pattern>*.jspf</url-pattern> <!-- jsp页面编码,它和page指令中的pagaEncoding特性一致--> <page-encoding>UTF-8</page-encoding> <!-- 允许使用jsp中的java,若为true,则禁止在jsp中使用java --> <scripting-invalid>false</scripting-invalid> <!-- 告诉web容器在所有属于该属性组的jsp的头部添加文件/WEB-INF/jsp/base.jspf --> <include-prelude>/WEB-INF/jsp/base.jspf</include-prelude> <!-- 告诉jsp转换器删除响应输出中的空白,只保留由指令、声明、脚本和其他JSP标签创建的文本,即可以产生干净的代码 --> <trim-directive-whitespaces>true</trim-directive-whitespaces> <!-- 默认的内容类型是text/html --> <default-content-type>text/html</default-content-type> </jsp-property-group> </jsp-config>
base.jspf:
<%@ page contentType="text/html; charset=utf-8" language="java"%> <%@ page import="cn.example.Ticket, cn.example.Attachment" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
listTickets.jsp:
<%@ page session="false" import="java.util.Map" %> <% @SuppressWarnings("unchecked") Map<Integer,Ticket> ticketDatabase = (Map<Integer, Ticket>)request.getAttribute("ticketDatabase"); %> <!DOCTYPE html> <html> <head> <title>留言板</title> </head> <body> <h2>留言板</h2> <a href=" <c:url value="/tickets"> <c:param name="action" value="create"/> </c:url> ">创建留言</a><br/><br/> <% if(ticketDatabase.size() == 0){ %><i>留言板中没有留言。</i><% } else{ for(int id : ticketDatabase.keySet()){ String idString = Integer.toString(id); Ticket ticket = ticketDatabase.get(id); %>留言 #<%= idString %> : <a href=" <c:url value="/tickets"> <c:param name="action" value="view"/> <c:param name="ticketId" value="<%= idString %>"/> </c:url> "><%=ticket.getSubject() %></a>(用户: <%= ticket.getCustomerName() %>) <br/> <% } } %> </body> </html>
ticketForm.jsp:
package cn.example; /* * 一个简单的POJO,表示着一个附件类 */ 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; } }
viewTicket.jsp:
<% String ticketId = (String) request.getAttribute("ticketId"); Ticket ticket = (Ticket) request.getAttribute("ticket"); %> <!DOCTYPE html> <html> <head> <title>留言版</title> </head> <body> <h2>留言 #<%=ticketId %>: <%= ticket.getSubject() %></h2> <i>用户 - <%=ticket.getCustomerName() %></i> <br/><br/> <i>内容:</i><br/> <%= ticket.getBody() %> <br/><br/> <% if(ticket.getNumberOfAttachments() > 0){ %>附件:<% int i = 0; for(Attachment a:ticket.getAttachments()){ if(i++ > 0) out.print(", "); %> <a href=" <c:url value="/tickets"> <c:param name="action" value="download"/>\ <c:param name="ticketId" value="<%= ticketId %>"/> <c:param name="attachment" value="<%=a.getName() %>"/> </c:url> "><%=a.getName() %> </a><% } } %><br/> <a href="<c:url value="/tickets"/>">返回留言板主页</a> </body> </html>
Attachment.java
package cn.example; /* * 一个简单的POJO,表示着一个附件类 */ 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.java:
package cn.example; /* * 一个简单的POJO,表示一个票据类 */ import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; public class Ticket { private String customerName; // 用户名 private String subject; // 评论内容的主题 private String body; // 评论内容的主体 // 使用哈希map表示附件数据库,以附件名为键,以附件为值 private Map<String, Attachment> attachments = new LinkedHashMap<String, Attachment>(); public String getCustomerName() { return customerName; } public void setCustomerName(String customerName) { this.customerName = customerName; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public Attachment getAttachment(String name){ return this.attachments.get(name); } public Collection<Attachment> getAttachments() { return this.attachments.values(); } public void addAttachments(Attachment attachment) { this.attachments.put(attachment.getName(), attachment); } public int getNumberOfAttachments(){ return this.attachments.size(); } public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } }
TicketServlet.java:
package cn.example; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.LinkedHashMap; import java.util.Map; import javax.servlet.RequestDispatcher; 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 jdk.nashorn.internal.ir.RuntimeNode.Request; @WebServlet( name = "ticketServlet", urlPatterns = {"/tickets"}, loadOnStartup = 1 ) //告诉web容器为该servlet提供文件上传支持 @MultipartConfig( // 告诉web容器文件必须达到5MB时才写入临时目录 fileSizeThreshold = 5_242_800, // 5MB // 上传的文件不能超过20MB maxFileSize = 20_971_520L, // 20MB // 不能接收超过40MB的请求 maxRequestSize = 41_942_040L // 40MB ) public class TicketServlet extends HttpServlet{ private volatile int TICKET_ID_SEQUENCE = 1; // 使用哈希map作为票据数据库 private Map<Integer, Ticket> ticketDatabase = new LinkedHashMap<>(); /* * 在doGet()方法中,根据请求参数的不同,把任务委托给相应的执行器 * 功能: * 显示创建票据页面 * 查看单个票据内容 * 下载附件 * 显示票据列表 */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf-8"); resp.setCharacterEncoding("utf-8"); String action = req.getParameter("action"); // 若不带action请求参数,设置默认值,即默认的行为是显示票据列表 if(action == null) action = "list"; switch (action) { case "create": this.showTicketForm(req,resp); break; case "view": this.viewTicket(req, resp); break; case "download": this.downloadAttachment(req, resp); break; default: this.listTickets(req, resp); break; } } /* * 创建新的票据 */ @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf-8"); resp.setCharacterEncoding("utf-8"); String action = req.getParameter("action"); if(action == null) action = "list"; switch(action){ case "create": this.createTicket(req, resp); break; case "list": default: resp.sendRedirect("tickets"); break; } } private void createTicket(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException{ //1.新建一个票据对象 Ticket ticket = new Ticket(); //2.以表单数据为源,设置票据相应的成员属性 ticket.setCustomerName(req.getParameter("customerName")); ticket.setSubject(req.getParameter("subject")); ticket.setBody(req.getParameter("body")); //3.处理文件上传 Part filePart = req.getPart("file1"); if(filePart != null && filePart.getSize() > 0){ Attachment attachment = this.processAttachment(filePart); if(attachment != null) ticket.addAttachments(attachment); } int id; synchronized (this) { id = this.TICKET_ID_SEQUENCE++; this.ticketDatabase.put(id, ticket); } resp.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; } private void listTickets(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { request.setAttribute("ticketDatabase", this.ticketDatabase); request.getRequestDispatcher("/WEB-INF/jsp/view/listTickets.jsp").forward(request, response); } private void downloadAttachment(HttpServletRequest req, HttpServletResponse resp) throws IOException { String idString = req.getParameter("ticketId"); Ticket ticket = this.getTicket(idString, resp); if(ticket == null) return; String name = req.getParameter("attachment"); if(name == null){ resp.sendRedirect("tickets?action=view&tickedId=" + idString); return; } Attachment attachment = ticket.getAttachment(name); if(attachment == null){ resp.sendRedirect("tickets?action=view&tickedId=" + idString); return; } // 强制浏览器询问用户是保存还是下载文件,而不是在浏览器打开该文件 resp.setHeader("Content-Disposition", "attachment; filename = " + attachment.getName()); // 设置内容类型是通用的、二进制内容类型,这样容器就不会使用字符编码对该数据进行处理 // 更加准确的应该使用附件的MIME内容类型 resp.setContentType("application/octet-stream"); // 使用ServletOutputStream将附件内容输出到响应中 ServletOutputStream stream = resp.getOutputStream(); stream.write(attachment.getContents()); } private void viewTicket(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { String idString = req.getParameter("ticketId"); Ticket ticket = this.getTicket(idString, resp); if(ticket == null) return; req.setAttribute("ticketId", idString); req.setAttribute("ticket", ticket); RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/jsp/view/viewTicket.jsp"); dispatcher.forward(req, resp); } private Ticket getTicket(String idString, HttpServletResponse resp) throws IOException{ if(idString == null || idString.length() == 0){ resp.sendRedirect("tickets"); return null; } try{ Ticket ticket = this.ticketDatabase.get(Integer.parseInt(idString)); if(ticket == null){ resp.sendRedirect("tickets"); return null; } return ticket; }catch(Exception e){ resp.sendRedirect("tickets"); return null; } } private void showTicketForm(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/jsp/view/ticketForm.jsp"); dispatcher.forward(request, response); } }
运行结果:
空白的留言板:
创建留言:
单个留言的详细信息:
附件下载:
非空白的留言板:
分析:
1.j在jsp可以结合java和html,方便编写动态页面
2.在jsp中,可以做几乎所有java类可以完成的事情,这会带来不安全的操作。若jsp开发者不熟悉java,而他们无限制地在jsp中使用java,会带来安全隐患。
3.表示层(jsp用于开发表示层)需要和业务逻辑层、数据持久层分割。
4.在jsp中显示动态内容,应该尽量避免使用java代码,可以使用jsp标签库替代java代码