再探 JSP、Servlet 与 Java Bean:用 MVC 模式实现一个简易留言板

文章目录

继续上一篇文章hhh,这篇的主要内容是用 MVC 模式重新实现一个相同功能的留言板,因为界面都一样所以这篇文章里就不放那些截图了,效果图可以参考前面一篇里的图片

JSP 中的 MVC

维基百科 上简单介绍了这个 JSP 的第二种模式架构:

jsp-model2

也就是 初识 JSP、Servlet 与 Java Bean 里提到的三个部分一一对应分别作为 V、C 和 M:

  • JSP 作为 View 进行页面内容的展示
  • Servlet 作为 Controller 接收来自客户端的请求,并转发给服务端进行对应的处理
  • Java Bean 作为 Model 进行实际的后台操作,如数据库的增删查改等

初步配置

首先需要在 /WEB-INF/ 目录下新建一个 web.xml,内容如下:

<?xml version="1.0" encoding="UTF-8"?>  
<web-app version="2.4"  
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
    http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <servlet>
        <servlet-name>Controller</servlet-name>
        <servlet-class>Controller</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>Controller</servlet-name>
        <url-pattern>/Controller</url-pattern>
    </servlet-mapping>
</web-app>  

作用如第一篇文章中的 Hello World 部分所说,就是把 Controller 这个类映射到 /Controller 这个 URL 上

当然还是要更改一下 Tomcat 的 Web 根目录,然后重启服务,这样 localhost:8080/Controller 就对应着 MVC 中的 Controller

因为默认的文件还是 index.jsp,所以用 <jsp:forward> 把它转发到 /Controller,并且为了方便和后面的 INSERT 操作区分开来,设置一个 operation 变量:

<jsp:forward page="/Controller">  
    <jsp:param name="operation" value="list"></jsp:param>
</jsp:forward>  

这样在访问 localhost:8080 或者 localhost:8080/index.jsp 时,实际渲染的就是 Controller 显示出来的内容了

新增数据处理

嗯……把 INSERT 部分放在 SELECT 部分前面是因为相对来说它的实现容易一点,毕竟 SELECT 还要传递所有数据到 View 的

操作上的代码其实和 General/insert(java).jsp 基本相同,主要还是在于用 Controller 分离了操作和请求处理,把 Model 也就是 Bean.java 开头加上 package 的设置 package Model;,然后放在 WEB-INF/classes/Model/Bean.java 编译成 class 文件,如果是直接命令行编译的话要注意编译时所处的路径,直接在 Model 文件夹里编译会报错的:

(xxx/WEB-INF/classes/)$ javac Model/Bean.java

Controller 放在 WEB-INF/classes/Controller.java,导入刚刚的 Bean 类,然后对 POST 请求也就是新留言数据进行转发处理,这里把数据库配置文件的读取也放在 Controller 里面一起作为参数传给 Model 了:

import java.io.*;  
import javax.servlet.*;  
import javax.servlet.http.*;  
import javax.xml.parsers.*;  
import org.w3c.dom.*;  
import Model.Bean;

public class Controller extends HttpServlet {  
    protected void doPost(
        HttpServletRequest req,
        HttpServletResponse res
    ) throws ServletException, IOException {
        if (req.getParameter("operation").equals("insert")) {
            req.setCharacterEncoding("UTF-8");
            res.setCharacterEncoding("UTF-8");

            try {
                DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
                DocumentBuilder db = dbf.newDocumentBuilder();
                Document config = db.parse(new File(
                    req.getServletContext().getRealPath("/WEB-INF/config.xml")
                ));

                Bean bean = new Bean();
                String result = bean.insert(
                    config.getElementsByTagName("database").item(0).getTextContent(),
                    config.getElementsByTagName("username").item(0).getTextContent(),
                    config.getElementsByTagName("password").item(0).getTextContent(),

                    new String(
                        req.getParameter("nickname").getBytes("ISO-8859-1"), "UTF-8"
                    ),
                    new String(
                        req.getParameter("message").getBytes("ISO-8859-1"), "UTF-8"
                    )
                );

                if (result.equals("OK")) {
                    res.sendRedirect("./result.jsp");
                } else {
                    req.setAttribute("fail", result);
                    getServletConfig().getServletContext()
                    .getRequestDispatcher("/result.jsp").forward(req, res);
                }
            } catch (Exception e) {
                req.setAttribute("fail", e.toString());
                getServletConfig().getServletContext()
                .getRequestDispatcher("/result.jsp").forward(req, res);
            }
        }
    }
}

Bean 的 insert() 函数如果操作成功则返回 "OK",否则返回错误信息的字符串

new String(  
    req.getParameter("message").getBytes("ISO-8859-1"), "UTF-8"
)

显然是用来处理 Unicode 的编码问题的,因为各种还不太清楚的原因,POST 过来的变量值默认是 ISO-8859-1 编码的 bytes[] 类型,无法直接表示中文,所以需要把它用 String 的构造函数转为 UTF-8 编码,否则如果传过来的值是 Unicode 的话,实际写入数据库的将会是一堆乱码

req.setAttribute("fail", "errMsg");  
getServletConfig().getServletContext()  
.getRequestDispatcher("/result.jsp").forward(req, res);

这个就是出错处理了,用 setAttribute() 设置一个 fail 变量,然后把页面 forward 到 result.jsp 里,在 result.jsp 就可以获取到这个变量的值,同样用 JSTL 里的 <c:choose> 进行判断和分流操作即可,具体可见第二篇文章的“异常处理”部分

如果没有出现错误,同样会被 sendRedirect() 重定向到 result.jsp,但是因为没有设置 fail 这个变量,也就是 <c:when test="${fail != null}"> 不成立,就会被分流到成功操作的部分,然后页面就会显示对应的操作成功提示

首页内容显示

Model

首页的内容展示首先需要获取数据库里每一行的信息,它们一般包含各种不同类型的值,所以需要对应写一个单独的 Msg 类,实际上同样是 Model 层的一个 Bean:

package Model;  
import java.sql.Timestamp;

public class Msg {  
    public int id;
    public String nickname;
    public String message;
    public Timestamp postTime;

    public Msg(
        int id,
        String nickname,
        String message,
        Timestamp postTime
    ) {
        this.id = id;
        this.nickname = nickname;
        this.message = message;
        this.postTime = postTime;
    }

    public int getId() { return id; }
    public String getNickname() { return nickname; }
    public String getMessage() { return message; }
    public Timestamp getPostTime() { return postTime; }
}

如第一篇文章写到的,按照规范它应该提供一个无参构造函数,然后有每一个变量的 getter 和 setter,这里同样为了遵循能偷懒就偷懒的原则不改了

Bean.java里需要增加一个 Msg 类型的变量数组 public Vector<Msg> messages;,然后写一个获取数据的函数,主要操作代码如下:

ResultSet rs = s.executeQuery("SELECT * FROM forum ORDER BY ID DESC");

messages = new Vector<Msg>();  
while (rs.next())  
    messages.add(new Msg(
        rs.getInt("ID"),
        rs.getString("nickname"),
        rs.getString("message"),
        rs.getTimestamp("postTime")
    ));

操作结束后所有的数据都存在 public 的变量 messages 里了,可以通过 Bean 对象直接读取

Controller

<jsp:forward> 传递变量是使用 GET 方式的,暂时也没找到能直接改成 POST 的方法,那么在 Controller 里增加(实际上也是 Override)doGet 函数,为了避免累赘就只贴核心部分的代码了:

protected void doGet(  
    HttpServletRequest req,
    HttpServletResponse res
) throws ServletException, IOException {
    // Other Code

    Bean bean = new Bean();
    String result = bean.getAllMsg(
        database, username, password
    );

    if (result.equals("OK")) {
        req.setAttribute("messages", bean.messages);
        getServletConfig().getServletContext()
        .getRequestDispatcher("/messages.jsp").forward(req, res);
    } else {
        req.setAttribute("fail", result);
        getServletConfig().getServletContext()
        .getRequestDispatcher("/result.jsp").forward(req, res);
    }

    // Other Code
}

报错和异常信息的处理方式还是和前面的新增数据处理一样,forward 到 result.jsp 显示报错信息,但是操作成功的话就会设置 messages 变量然后 forward 到 messages.jsp,实际上也就是一开始的 index.jsp 页面显示出来的实际内容

View

messages.jsp 就是首页显示内容的基本框架了,首先将 JSTL 相关的 jstl.jarstandard.jar 两个包放入 WEB-INF/lib/ 目录下,然后在 JSP 文件开头进行基本的设置并引入需要使用的几个包:

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>  
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>  
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>  

页面结构用 HTML 设计好之后,用 JSTL 类似的方式即可完成数据的显示操作,只是不需要通过 <sql:query> 等标准标签库操作进行数据库查询了,把 <c:forEach> 里面的 items 属性更改为 forward 的时候传的变量 messages 即可:

<c:forEach items="${messages}" var="message">  

实际上除了 ${messages} 以外,还可以通过别的方式获取这个变量:

<%@ page import="Model.Msg, java.util.Vector"%>  
<%  
    Vector<Msg> m = (Vector<Msg>)request.getAttribute("messages");
    out.println(m.elementAt(0).nickname);
%>

导入相关的包,然后用 getAttribute() 函数获取 request 对象里的 messages,这个时候返回的是一个对象,需要用 (Vector<Msg>) 进行类型转换,这样得到的 m 就是一个 Msg 数组了,可以通过 m.elementAt(n).xxx 获取它第 n 位的 xxx 变量值

两个小小的坑

实际上在做这个首页内容显示的时候碰到两个坑: postTime 的类型问题和 Msg 的 getter 问题

postTime 在 MySQL 里是 TIMESTAMP 类型,如果像我一开始那样直接用 Java 的 Date 类的话,会只有日期而没有时间,也就是显示出来的时间全部是 00:00:00,后来我尝试用 String 类型处理,然后当然是各种地方报类型不匹配的错误啦hhh

不过这个问题也没耗多少时间,马上就查到了 java.sql.Timestamp 这个类,可以直接用来存 TIMESTAMP 类型的值

getter 的问题还是因为没有遵守 Java Bean 的规范以及对 JSTL 不太了解造成的,比如:

<c:out value="${message.id}"/>  

message 是 messages 数组里的一个元素,也就是 Msg 类型的一个变量,JSTL 在读取它的 id 属性的时候,内部的实现应该是去找这个类里对应的 getter,也就是 getId() 函数,但是我一开始没写任何一个 getter,这就导致它一直在报错而我却没反应过来是什么原因,尤其是我还用刚刚提到的 m.elementAt(0).nickname 方法证明了变量值的传递是没问题的

所以规范还是得遵守啊23333,顺便还去了解了一下 JSP 使用 Java Bean 的一些操作,大概这就是为什么 Java Bean 有那些要求:

<jsp:useBean id="t" class="test.bean">  
    <jsp:setProperty name="t" property="nickname" value="xxx"/>
</jsp:useBean>  
<jsp:getProperty name="t" property="nickname"/>  

这里面的 <jsp:useBean> 就对应着无参构造参数的规范,因为用 id 属性的值作为标识符直接 new 一个对象显然方便了很多,然后 <jsp:setProperty><jsp:getProperty> 就对应着 setter 和 getter 以及他们的函数名命名要求了

结语

关于 JSP、Servlet、Java Bean、JDBC、JSTL 的简单尝试和实验就到这里了,也没干什么别的,我主要的我就是魔改了一下之前写的一个辣鸡留言板,然后用各种方式实现了一下它的基本功能

如果说还有一点什么的话,那就是 Typora 导出 PDF 的时候行内代码的左右尖括号后面竟然都莫名其妙多了个分号

这个 Export to PDF 呢,我们就有几年用 Typora,旁边一个小分号,最痛苦的就是 Typora 把这个分号弄到里面,导出的时候一下子就……然后就去找各种解决方案,这个就是导出最痛苦的,而且这个效率 efficiency……

啊不对我在写什么,真是太容易走火入膜了23333 实际上后来找到的解决办法是先导出成 HTML 文件,然后在浏览器里打开再打印成 PDF,我怎么这么机智啊(

最后一步

水完 Java 实验报告,卸载 Tomcat(逃

估计是不可能再接触这一块的开发了吧,本来也就是用来 xjb 写个辣鸡作业

还有,PHP 是世界上坠吼的语言!(再逃

系列文章地址

这些 Java Web 相关的内容是 Java 课的最后一次实验做的,因为觉得多少有一些总结的意义,就稍微整理了一下并分成三篇博客文章发布:

  1. 初识 JSP、Servlet 与 Java Bean:Tomcat 的配置和几个基本的 Hello World
  2. 初识 JDBC 与 JSTL:配合 MySQL 实现一个简易留言板
  3. 再探 JSP、Servlet 与 Java Bean:用 MVC 模式实现一个简易留言板