1、项目需求
为该公司网站中添加一个交互性的客户支持应用程序。它需要能够让用户提出问题或支持票据,并且员工也可以对这些查询做出响应。支持票据和评论都应该支持文件附件。对于紧急的事件,客户能够进入一个有特定客户支持代表参与的聊天窗口。并且最重要的是,作为跨国公司的网站,整个应用程序能够实现本地化,要求支持公司所需要的所有语言,并且该应用程序还必须非常安全。这些要求并不过分,对吧,明天上线ok。哈哈哈哈
2、创建项目
2.1、启动idea创建maven项目
选择maven和配置文件
工程名和项目路径
之后点击完成。
为了在pom.xml文件中添加依赖之后自动引入jar,点击右下角红圈的Enable Auto-Import选项
配置全局tomcat
然后就能在Run/Debug Configurations里设置tomcat了
(注意:如果指定了项目的url路径那么application context也要指定路径,见下图红圈)
添加完成tomcat后点击运行,运行成功后如下图所示,项目部署成功
(注意:请先在命令行停止已经运行的系统的tomcat服务,可使用命令systemctl stop tomcat8,否则无法启动idea的tomcat服务)
3、Customer-Support-v1
3.1、v1的功能,由三个页面组成,通过doGet处理,一个票据列表,一个创建票据的页面和一个查看单个票据的页面。还支持下载某个ticket票据文件的附件,以及接受POST请求用于创建新的票据。
3.2、pom.xml加入servlet依赖,scope详解
3.3、POJO类
首先右键main选择Mark Directory as选择Sources Root,这样才能创建类,创建pojo包并且创建Ticket类和Attachment类,如下
/**
* @Author ljs
* @Description TODO
* @Date 2018/8/3 23:05
**/
public class Attachment {
private String name;
private byte[] contents;
//省略get和set
}
首先,一个票据可以有多个附件,然后这些附件有名字,所以我们创建一个LinkedMap键值对来存储而不是一个List,这样就可以通过名字(键)来获取某个附件。这里先不写dao层,所有增查功能对应的方法写在model类里。主要有四个接口:
- getAttachment(String name),通过附件名字来获取单个附件,
- getAttachments(),这里是返回所有的附件,注意返回类型是一个集合Collection而不是一个Map,因为我们只需要返回附件实例,而不需要返回它们对应的名字,附件实例里其实已经有属性name。
- addAttachment(Attachment attachment)添加附件
- getNumberOfAttachments() 返回该票据附件的个数
注意在这个类里的我们对attachments这个私有属性开放接口不再是get和set,而是addAttachment,getAttachment,getAttachmentsget和NumberOfAttachments,所以attachments没有get和set方法。
/**
* @Author ljs
* @Description TODO
* @Date 2018/8/3 22:56
**/
public class Ticket {
private String customerName; //票据名
private String subject; //票类型
private String body;
private Map<String, Attachment> attachments = new LinkedHashMap<>(); //和附件是一对多
public Attachment getAttachment(String name)
{
return this.attachments.get(name);
}
public Collection<Attachment> getAttachments()
{
return this.attachments.values();
}
public void addAttachment(Attachment attachment)
{
this.attachments.put(attachment.getName(), attachment);
}
public int getNumberOfAttachments()
{
return this.attachments.size();
}
//省略get和set
}
3.4、视图层与控制层分离
没分离之前向响应中输出动态的html代码全都写在servlet里的,非常不方便,所以使用jsp让业务逻辑与视图分离。首先加入依赖,因为jstl实现定义了相对旧版jsp和servlet规范的依赖,它们与当前版本的jsp和servlet规范的maven artifact id不同,所以使用exclusions将它们排除。也就是说加入了jstl依赖,但是这个依赖又依赖于旧版的jsp和servlet,根据maven的依赖传递,会把旧版的jsp和servlet依赖加入这个项目中,跟上面的servlet和jsp依赖冲突,所以使用exclusions排除依赖。
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>javax.servlet.jsp.jstl-api</artifactId>
<version>1.2.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.servlet.jsp.jstl</artifactId>
<version>1.2.2</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</exclusion>
<exclusion>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
</exclusion>
<exclusion>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl-api</artifactId>
</exclusion>
</exclusions>
</dependency>
3.5、jsp
如果每个jsp都有相似的属性,那么在每个jsp文件的顶部重复添加page指令是非常麻烦的工作。我们可以在web.xml里设置通用的jsp属性。标签<jsp-config>是在<web-app>标签下的,该标签可以包含任意数目的<jsp-property-group>标签。
下面这个属性组表示匹配项目中所有.jsp和.jspf的文件,把匹配到的所有jsp文件编码都设置为utf8类型为text/html,并且包含/WEB-INF/jsp/base.jspf这个jsp片段,<trim-directive-whitespaces>这个命令可以使jsp输出的html时去除多余的空行(jsp上使用EL和tag会产生大量的空格和空行)。
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspf</url-pattern>
<page-encoding>utf-8</page-encoding>
<include-prelude>/WEB-INF/jsp/base.jsp</include-prelude>
<trim-directive-whitespaces>true</trim-directive-whitespaces>
<default-content-type>text/html</default-content-type>
</jsp-property-group>
</jsp-config>
注意:intellij idea默认创建的web.xml版本为2.3版本,对应的jstl是1.1版本,最好替换为3.1版本的web.xml,对应的jstl是1.2版本是1.2版本,所以使用idea默认的web.xml是识别不了<jsp-config>标签的。修改如下重启:
<?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">
base.jsp,1、导入类2、声明jstl核心代码库。
<%@ page import="pojo.Ticket, pojo.Attachment" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
listTickets.jsp
1、禁止该jsp使用会话
2、该页面需要遍历显示ticket,所以需要ticketDatabase,这个值在servlet的listTickets方法中设置,存储到请求中,当转发到该页面是,通过request.getAttribute()取出,因为getAttribute()返回Object对象,所以需要强制转换,将对象强制转换是一个未检查操作,所以需要抑制警告。
<%@ page session="false" import="java.util.Map" %>
<%
@SuppressWarnings("unchecked")
Map<Integer, Ticket> ticketDatabase =
(Map<Integer, Ticket>)request.getAttribute("ticketDatabase");
%>
<!DOCTYPE html>
<html>
<head>
<title>Customer Support</title>
</head>
<body>
<h2>Tickets</h2>
<a href="<c:url value="/tickets">
<c:param name="action" value="create" />
</c:url>">Create Ticket</a><br /><br />
<%
if(ticketDatabase.size() == 0)
{
%><i>There are no tickets in the system.</i><%
}
else
{
for(int id : ticketDatabase.keySet())
{
String idString = Integer.toString(id);
Ticket ticket = ticketDatabase.get(id);
%>Ticket #<%= idString %>: <a href="<c:url value="/tickets">
<c:param name="action" value="view" />
<c:param name="ticketId" value="<%= idString %>" />
</c:url>"><%= ticket.getSubject() %></a> (customer:
<%= ticket.getCustomerName() %>)<br /><%
}
}
%>
</body>
</html>
viewTicket.jsp
<%@ page session="false" %>
<%
String ticketId = (String)request.getAttribute("ticketId");
Ticket ticket = (Ticket)request.getAttribute("ticket");
%>
<!DOCTYPE html>
<html>
<head>
<title>Customer Support</title>
</head>
<body>
<h2>Ticket #<%= ticketId %>: <%= ticket.getSubject() %></h2>
<i>Customer Name - <%= ticket.getCustomerName() %></i><br /><br />
<%= ticket.getBody() %><br /><br />
<%
if(ticket.getNumberOfAttachments() > 0)
{
%>Attachments: <%
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 /><br /><%
}
%>
<a href="<c:url value="/tickets" />">Return to list tickets</a>
</body>
</html>
ticketForm.jsp
- multipart/form-data表明该表单可以接受上传
- hidden不会在页面实现出来,但是提交表单的时候还是会传给后台
<%@ page session="false" %>
<!DOCTYPE html>
<html>
<head>
<title>Customer Support</title>
</head>
<body>
<h2>Create a Ticket</h2>
<form method="POST" action="tickets" enctype="multipart/form-data">
<input type="hidden" name="action" value="create"/>
Your Name<br/>
<input type="text" name="customerName"><br/><br/>
Subject<br/>
<input type="text" name="subject"><br/><br/>
Body<br/>
<textarea name="body" rows="5" cols="30"></textarea><br/><br/>
<b>Attachments</b><br/>
<input type="file" name="file1"/><br/><br/>
<input type="submit" value="Submit"/>
</form>
</body>
</html>
3.6、Servlet
一个servlet可以通过请求参数action的不同分别调不同的相应方法。
showTicketForm,viewTicket,listTickets三个方法主要调用getRequestDispatcher转发到对应的三个jsp
createTicket方法,在提交表单post请求之后就调用该方法,该方法提取表单的数据封装成ticket对象并且存入map中,最后调用sendRedirect重定向到view。 使用getRequestDispatcher方法转发请求和使用 sendRedirect()方法重定向的区别
保护共享资源,方法中创建的对象和变量在方法执行过程中的安全的,其他线程无法访问它。但是Servlet中的静态变量和实例变量都可以被多个线程访问的。所以当在使用这些变量的时候,对于多个请求(多线程)来说,可以使用同步代码块synchronized(this)来保证多个线程无法同时执行相同代码,使代码块具有排他性,避免出现多个ticketId相同而map是不重复的,会抛出异常。而且我们可以给变量加入volatile,避免一致性问题(其他线程读到变量修改之前的值)。
processAttachment,把表单提交的文件封装成attachment对象。先inputStream读到内存,然后再outputstream封装到对象的属性中。servlet3.1新增的getSubmittedFileName()识别文件上传之前的名字。
getTicket通过id获取对应的Ticket
downloadAttachment,Content-Disposition强制浏览器询问客户是保存还是下载,并且不会在线打开,google直接下载,360有询问。application/octet-stream,容器不会使用字符编码对该数据进行处理,最好还是使用MIME内容类型。最后使用servletOutputString将文件内容输出到相应中。
@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
{
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(request, response);
break;
case "view":
this.viewTicket(request, response);
break;
case "download":
this.downloadAttachment(request, response);
break;
case "list":
default:
this.listTickets(request, response);
break;
}
}
@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;
}
}
private void showTicketForm(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
request.getRequestDispatcher("/WEB-INF/jsp/view/ticketForm.jsp")
.forward(request, response);
}
private void viewTicket(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
String idString = request.getParameter("ticketId");
Ticket ticket = this.getTicket(idString, response);
if(ticket == null)
return;
request.setAttribute("ticketId", idString);
request.setAttribute("ticket", ticket);
request.getRequestDispatcher("/WEB-INF/jsp/view/viewTicket.jsp")
.forward(request, response);
}
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.getAttachment(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());
}
private void listTickets(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
request.setAttribute("ticketDatabase", this.ticketDatabase);
request.getRequestDispatcher("/WEB-INF/jsp/view/listTickets.jsp")
.forward(request, response);
}
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;
}
private Ticket getTicket(String idString, HttpServletResponse response)
throws ServletException, IOException
{
if(idString == null || idString.length() == 0)
{
response.sendRedirect("tickets");
return null;
}
try
{
Ticket ticket = this.ticketDatabase.get(Integer.parseInt(idString));
if(ticket == null)
{
response.sendRedirect("tickets");
return null;
}
return ticket;
}
catch(Exception e)
{
response.sendRedirect("tickets");
return null;
}
}
}