2. J2EE - MVC 設計模式進階

一、需求

之前的一篇博文講了最簡單的 MVC 案例,其中有很多不成熟的地方。
首先數據庫方面,我們可以使用連接池,也可以寫一個 DAO 的基類供具體的實現類去繼承。
在之前的 Servlet 中,一個 Servlet 只可以處理一種請求。這次我們將使用反射使一個 Servlet 可以同時處理多個請求。
這次的案例完成後如下圖所示,可以在三個輸入欄中添加信息,點擊“search”進行模糊查詢,也可以添加新用戶,也可以在表格中刪除信息。
完成效果.png

二、代碼解析

通常來說,當我們寫一個項目或者案例時,都是從底層開始寫起,這樣可以逐層測試以便排查錯誤。下面我們就從底層數據庫開始寫起。
這次的數據庫中,用戶 ID 是主鍵,它具有自增的屬性,每條記錄還有姓名,地址,電話三個子段,其中姓名有唯一的屬性。

1. 數據庫交互層

第一步:建立數據庫連接池 & 獲取連接

數據庫連接池是指在程序啓動時建立足夠的數據庫連接,並將這些連接組成一個連接池,由程序動態地對池中的連接進行申請,使用,釋放。
簡單地說,程序啓動時就建立了多個數據庫連接放在緩存區,用戶需要時可以馬上取出,使用完之後放回連接池即可,不用每次需要操作時就去做建立數據庫連接這種耗時的操作了。

導入 c3p0 數據庫連接池以及 mysql-connector 的 jar 包。

在 src 目錄下建立一個名爲 c3p0-config.xml (必須是這個名字)的配置文件,然後在其中配置用戶名,密碼,驅動類,數據庫 url 等信息,具體如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>

  <named-config name="mvc_example">

    <property name="user">root</property>
    <property name="password">root</property>
    <property name="driverClass">com.mysql.jdbc.Driver</property>
    <property name="jdbcUrl">jdbc:mysql://localhost/lister</property>

    <property name="acquireIncrement">5</property>
    <property name="initialPoolSize">10</property>
    <property name="minPoolSize">10</property>
    <property name="maxPoolSize">50</property>
    <property name="maxStatements">20</property> 
    <property name="maxStatementsPerConnection">5</property>

  </named-config>

</c3p0-config>

配置完 c3p0 連接池以後,我們就可以通過它的方法來獲取連接了。
新建 JDBCUtils 類,在 static 代碼塊中新建數據庫連接池,這樣連接池只會被初始化一次。

public class JDBCUtils {

    // dataSource 只有一份且只被初始化一次
    private static DataSource dataSource;

    static {
        dataSource = new ComboPooledDataSource("mvc_example");
    }

    // 獲取數據庫連接
    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    // 關閉數據庫連接
    public static void releaseConnection(Connection connection) {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

至此,我們已經可以通過 JDBCUtils 的靜態方法獲取和釋放連接。

第二步:DAO 基類

定義一個帶泛型的通用的數據庫基類 DAO< T >,這個 T 就是泛型類,它隨着我們操作的實體類的不同而變化。
在它的構造方法中我們可以通過反射將具體的類 T 取出。它也有一系列基礎的數據庫操作的方法,這個類的派生類都可以直接使用這些操作。

導入 common-dbutils jar 包

dbutils 大大簡化了數據庫操作,以前我們需要將數據庫信息一條一條取出,再把屬性一個一個賦值給實體類。
而在 dbutils 中,BeanHandler 可以操作一個實體類對象,直接把一條數據庫記錄賦值給實體類,它甚至還有 BeanListHandler 用於操作多條記錄,程序員只需要一行代碼,就可以將數據庫的記錄取到內存,具體如下所示。

public class DAO<T> {

    private QueryRunner queryRunner = new QueryRunner();

    private Class<T> clazz;

    public DAO() {
        // 獲得帶有泛型的父類
        Type superClass = getClass().getGenericSuperclass();
        // ParameterizedType 是參數化類型,即泛型
        if (superClass instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) superClass;
            // 因爲泛型可能有多個,所以使用參數類型數組保存
            Type[] typeArgs = parameterizedType.getActualTypeArguments();
            if (typeArgs != null && typeArgs.length > 0) {
                if (typeArgs[0] instanceof Class) {
                    clazz = (Class<T>) typeArgs[0];
                }
            }
        }
    }

    // 獲取到數據庫中的某個字段的值
    public <E> E getValue(String sql, Object ... args) {
        Connection connection = null;
        try {
            connection = JDBCUtils.getConnection();
            return (E) queryRunner.query(connection, sql, new ScalarHandler(), args);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.releaseConnection(connection);
        }

        return null;
    }

    // 獲取到數據庫中的 N 條記錄的列表
    public List<T> getList(String sql, Object ... args) {
        Connection connection = null;
        try {
            connection = JDBCUtils.getConnection();
            return queryRunner.query(connection, sql, new BeanListHandler<>(clazz), args);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.releaseConnection(connection);
        }

        return null;
    }

    // 獲取到數據庫中的一條記錄
    public T get(String sql, Object ... args) {
        Connection connection = null;
        try {
            connection = JDBCUtils.getConnection();
            return queryRunner.query(connection, sql, new BeanHandler<>(clazz), args);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.releaseConnection(connection);
        }

        return null;
    }

    // 進行 insert, delete, update 操作
    public void update(String sql, Object ... args) {
        Connection connection = null;
        try {
            connection = JDBCUtils.getConnection();
            queryRunner.update(connection, sql, args);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.releaseConnection(connection);
        }
    }
}

DAO 具體實現

在建立具體的數據庫操作類之前,我們先把對應數據庫信息的實體類寫出來。
實體類沒什麼好說的,但要注意第二個構造方法中,我們不傳入 ID,因爲數據庫會自動生成。

public class Customer {

    private Integer id;
    private String name;
    private String address;
    private String phone;

    public Customer() {
        super();
    }

    public Customer(String name, String address, String phone) {
        super();
        this.name = name;
        this.address = address;
        this.phone = phone;
    }

    public Integer getId() { return id; }

    public String getName() { return name; }

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

    public String getAddress() { return address; }

    public void setAddress(String address) { this.address = address; }

    public String getPhone() { return phone; }

    public void setPhone(String phone) { this.phone = phone; }

    @Override
    public String toString() {
        return "Customer [id=" + id + ", name=" + name + ", address=" + address + ", phone=" + phone + "]";
    }
}

對於具體的數據庫操作類,我們可以先定義一個接口。

public interface CustomerDAO {

    public List<Customer> fuzzySearch(SearchInfo searchInfo);
    public List<Customer> getAll();
    public void add(Customer customer);
    public Customer get(Integer id);
    public void delete(Integer id);
    public long getNameCount(String name);
}

接下來就是具體的 DAO 實現類了。
第一個模糊查詢方法可以先不看,其中的 SearchInfo 實體類在下面給出。
最後一個方法用於檢測數據庫中某個名字是否已經存在,因爲數據庫中的名字是唯一的,在我們新建用戶時,不能再使用已經存在的名字。

public class CustomerDAOImpl extends DAO<Customer> implements CustomerDAO {

    // 通過表單提交的信息進行模糊查詢
    @Override
    public List<Customer> fuzzySearch(SearchInfo searchInfo) {
        String sql = "select id, name, address, phone from customer "
                + "where name like ? and address like ? and phone like ?";
        return getList(sql, searchInfo.getName(), searchInfo.getAddress(), searchInfo.getPhone());
    }

    // 獲取所有的信息
    @Override
    public List<Customer> getAll() {
        String sql = "select id, name, address, phone from customer";
        return super.getList(sql);
    }

    @Override
    public void add(Customer customer) {
        String sql = "insert into customer(name, address, phone) values (?, ?, ?)";
        update(sql, customer.getName(), customer.getAddress(), customer.getPhone());
    }

    @Override
    public Customer get(Integer id) {
        String sql = "select id, name, address, phone from customer where id = ?";
        return get(sql, id);
    }

    @Override
    public void delete(Integer id) {
        String sql = "delete from customer where id = ?";
        update(sql, id);
    }

    @Override
    public long getNameCount(String name) {
        String sql = "select count(id) from customer where name = ?";
        return getValue(sql, name);
    }
}

模糊查詢

模糊查詢使用的 SearchInfo 實體類,如果你瞭解數據庫語句,結合上面的方法就能看懂。

public class SearchInfo {

    private String name;
    private String address;
    private String phone;

    public SearchInfo(String name, String address, String phone) {
        super();
        this.name = name;
        this.address = address;
        this.phone = phone;
    }

    public String getName() {
        if (name == null) {
            return "%%";
        } else {
            return "%" + name + "%";
        }
    }

    public String getAddress() {
        if (address == null) {
            return "%%";
        } else {
            return "%" + address + "%";
        }
    }

    public String getPhone() {
        if (phone == null) {
            return "%%";
        } else {
            return "%" + phone + "%";
        }
    }

}

到此,數據庫部分全部完成。

2. Servlet & jsp

開頭我們說過,我們可以通過反射讓一個 Servlet 處理多個請求,那麼如何做到呢?
Servlet 提供了一種 <url-pattern>,讓我們可以在 jsp 中使用 *.xxx 的請求來匹配同一個 Servlet。
我們新建一個 Servlet,並將 <url-pattern> 配置爲 *.customer,如下所示。

@WebServlet("*.customer")
public class CustomerServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private CustomerDAO dao = new CustomerDAOImpl();
    public CustomerServlet() {
        super();
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doPost(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // ......
    }
}

新建 index.jsp,body 標籤下的代碼如下所示。
我們使用 queryInfo.customer 的請求來進行查詢,使用 deleteInfo.customer 的請求來進行刪除。這些請求最終都會被匹配到上面的 CustomerServlet 中。

<body>

    <form action="queryInfo.customer" method="post">
        <table>
            <tr>
                <td>姓名:</td>
                <td><input type = "text" name = "name"/></td>
            </tr>
            <tr>
                <td>地址:</td>
                <td><input type = "text" name = "address"/></td>
            </tr>
            <tr>
                <td>電話:</td>
                <td><input type = "text" name = "phone"/></td>
            </tr>
            <tr>
                <td><input type = "submit" value = "search"/></td>
                <td><a href = "addcustomer.jsp">添加新客戶</a></td>
            </tr>
        </table>

    </form>

    <br><br>

    <% List<Customer> list = (List<Customer>)request.getAttribute("customers"); %>
    <table border = "1" cellpadding = "10" cellspacing = "0">
        <tr>
            <td>用戶ID</td>
            <td>姓名</td>
            <td>地址</td>
            <td>電話</td>
            <td>刪除操作</td>
        </tr>
        <% 
            if (list != null && list.size() > 0) {
                for (int i = 0; i < list.size(); i++) {
         %>
        <tr>
            <td><%=list.get(i).getId() %></td>
            <td><%=list.get(i).getName() %></td>
            <td><%=list.get(i).getAddress() %></td>
            <td><%=list.get(i).getPhone() %></td>
            <td><a href="deleteInfo.customer?id=<%=list.get(i).getId() %>" class="delete">刪除</a></td>
        </tr>
        <% } }%>

    </table>

</body>

新增用戶的 addcustomer.jsp 如下所示,很顯然,它的請求 addInfo.customer 也會由 CustomerServlet 處理。

<body>

    <%
        Object msg = request.getAttribute("message");
        if (msg != null) {
    %>

        <font color="red"><%=msg %></font>

    <% } %>

    <form action="addInfo.customer" method="post">

        <table>
            <tr>
                <td>姓名:</td>
                <td><input type = "text" name = "name" 
                    value = "<%=request.getParameter("name") == null ? "" : request.getParameter("name")%>"/></td>
            </tr>
            <tr>
                <td>地址:</td>
                <td><input type = "text" name = "address"
                    value = "<%=request.getParameter("address") == null ? "" : request.getParameter("address")%>"/></td>
            </tr>
            <tr>
                <td>電話:</td>
                <td><input type = "text" name = "phone"
                    value = "<%=request.getParameter("phone") == null ? "" : request.getParameter("phone")%>"/></td>
            </tr>
            <tr>
                <td><input type = "submit" value = "添加用戶"/></td>
            </tr>
        </table>

    </form>

</body>

在 CustomerServlet 中,我們將通過請求的名字來辨別不同的請求。
比如接收到 addInfo.customer 請求時 ,我們抽取其中的 addInfo 字符串,再定義一個與該字符串同名的 addInfo 方法。
爲什麼需要同名的方法呢?
我們抽取出 addInfo 字符串時,可以通過反射的 invoke(…) 尋找 addInfo 方法並執行,只有方法名與該字符串相同,方法纔會被正確找到並執行。

CustomerServlet 完整代碼如下:

@WebServlet("*.customer")
public class CustomerServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    private CustomerDAO dao = new CustomerDAOImpl();

    public CustomerServlet() {
        super();
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doPost(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");

        // 獲得請求的 ServletPath 並抽取其中的方法名字段
        String methodName = request.getServletPath();
        methodName = methodName.substring(1, methodName.length() - 9);

        // 通過方法名找到方法並調用
        try {
            Method method = getClass().getDeclaredMethod(methodName, 
                    HttpServletRequest.class, HttpServletResponse.class);
            method.invoke(this, request, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 查詢指定的客戶信息
     * @param request
     * @param response
     */
    public void queryInfo(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {

        // 獲取表單提交的參數
        // 通過該參數進行模糊查詢
        String name = request.getParameter("name");
        String address = request.getParameter("address");
        String phone = request.getParameter("phone");
        SearchInfo searchInfo = new SearchInfo(name, address, phone);
        List<Customer> list = new ArrayList<>();
        list = dao.fuzzySearch(searchInfo);

        // 將查詢到的信息放入請求中
        // 轉發到 jsp 頁面進行顯示
        request.setAttribute("customers", list);
        request.getRequestDispatcher("/index.jsp").forward(request, response);
    }

    /**
     * 添加客戶信息
     * @param request
     * @param response
     */
    public void addInfo(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        String name = request.getParameter("name");
        String address = request.getParameter("address");
        String phone = request.getParameter("phone");

        // 如果用戶名被佔用
        if (dao.getNameCount(name) > 0) {
            request.setAttribute("message", "用戶名" + name + "已被佔用,請重新選擇!");
            request.getRequestDispatcher("/addcustomer.jsp").forward(request, response);
            return;
        }
        // 如果用戶名沒有被佔用
        Customer customer = new Customer(name, address, phone);
        dao.add(customer);
        response.sendRedirect("success.jsp");
    }

    /**
     * 刪除客戶信息
     * @param request
     * @param response
     */
    public void deleteInfo(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {

        try {
            int id = Integer.parseInt(request.getParameter("id"));
            dao.delete(id);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 請求重定向
        response.sendRedirect("queryInfo.customer");
    }
}

三、總結

這個案例比上個案例更能凸顯 MVC 的層次化設計。
這裏的數據庫交互層的設計很好的使用了面向對象的思想,其中的 DAO 基類在任何的項目中都可以複用,只需要重寫一個繼承於它的 DAO 類即可。
而這個案例中的控制器 Servlet 可以執行所有的請求,更好地充當了 Controller 的角色。這樣一來,整個項目的結構就清晰了很多。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章