一、需求
之前的一篇博文講了最簡單的 MVC 案例,其中有很多不成熟的地方。
首先數據庫方面,我們可以使用連接池,也可以寫一個 DAO 的基類供具體的實現類去繼承。
在之前的 Servlet 中,一個 Servlet 只可以處理一種請求。這次我們將使用反射使一個 Servlet 可以同時處理多個請求。
這次的案例完成後如下圖所示,可以在三個輸入欄中添加信息,點擊“search”進行模糊查詢,也可以添加新用戶,也可以在表格中刪除信息。
二、代碼解析
通常來說,當我們寫一個項目或者案例時,都是從底層開始寫起,這樣可以逐層測試以便排查錯誤。下面我們就從底層數據庫開始寫起。
這次的數據庫中,用戶 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 的角色。這樣一來,整個項目的結構就清晰了很多。