Customer-Support-v1

1、项目需求

为该公司网站中添加一个交互性的客户支持应用程序。它需要能够让用户提出问题或支持票据,并且员工也可以对这些查询做出响应。支持票据和评论都应该支持文件附件。对于紧急的事件,客户能够进入一个有特定客户支持代表参与的聊天窗口。并且最重要的是,作为跨国公司的网站,整个应用程序能够实现本地化,要求支持公司所需要的所有语言,并且该应用程序还必须非常安全。这些要求并不过分,对吧,明天上线ok。哈哈哈哈

2、创建项目

2.1、启动idea创建maven项目
image.png

image.png

选择maven和配置文件


image.png

工程名和项目路径
image.png

之后点击完成。

为了在pom.xml文件中添加依赖之后自动引入jar,点击右下角红圈的Enable Auto-Import选项


image.png

配置全局tomcat


image.png

然后就能在Run/Debug Configurations里设置tomcat了
(注意:如果指定了项目的url路径那么application context也要指定路径,见下图红圈)
image.png

image.png

添加完成tomcat后点击运行,运行成功后如下图所示,项目部署成功
(注意:请先在命令行停止已经运行的系统的tomcat服务,可使用命令systemctl stop tomcat8,否则无法启动idea的tomcat服务)


image.png

3、Customer-Support-v1

3.1、v1的功能,由三个页面组成,通过doGet处理,一个票据列表,一个创建票据的页面和一个查看单个票据的页面。还支持下载某个ticket票据文件的附件,以及接受POST请求用于创建新的票据。
3.2、pom.xml加入servlet依赖,scope详解
image.png
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;
        }
    }
}

3.7、上传到git
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容