MVC设计模式案例(上)
标签(空格分隔): JavaWeb
需求
因为很简单,所以就不写需求分析了。主要有一下几点:
- 创建一个数据表,包含id,name,address,phone
- 可以在页面上对整张表进行查询
- 可以创建一个新的条目
- 可以对原有的数据进行修改和删除
- 在创建或修改的情况下,验证用户名是否已经被使用,并给出提示
- 支持模糊查询
分析
使用到的技术
- MVC 设计模式:JSP、Servlet,POJO
- 数据库使用 MySQL
- 连接数据库需要使用 C3P0 数据库连接池
- JDBC 工具采用 DBUtils
- 页面上的提示操作使用 jQuery
技术难点
- 多个请求如何使用一个 Servlet ?
- 如何模糊查询 ?
- 如何在创建 或 修改 的情况下,验证用户名是否已经被使用,并给出提示
架构
MySQL
Model:用于存储数据
Model:DAO
- 获取数据库连接
- 执行CRUD操作
- 返回结果
注意:
1. 不能跨层访问
2. 只能自上向下以来,而不能自下向上依赖
Controller:Servlet
- 获取请求信息:获取请求参数(以供与JSP进行交互)
- 验证请求参数的合法性:验证失败需要返回页面并给出提示信息
- 把请求参数封装为一个JavaBean
- 调用DAO的方法获取返回的结果
- 把返回结果放入把request中
- 响应页面:转发、重定向
View:JSP
- 呈现数据:从request中获取Servlet中放入的属性
- 接收用户的输入
- 编写JS代码给出对应的提示
DAO层设计与实现
设计思路
- 创建数据表
- 创建一个实体类Customer
- 创建一个JDBCUtils,使用c3p0数据库连接池
- 创建一个DAO类,用来封装基本的CRUD操作,具体通过DBUtils工具来实现
- 创建一个CustomerDAO接口,声明了具体业务相关的方法
- 创建CustomerDAO接口的实现类CustomerDAOJDBCImpl,继承了DAO类。通过DAO类中的方法来实现CustomerDAO的方法
创建数据表
Create table customers(
id int primary key auto_increment,
name varchar(30) not null unique,
address varchar(30),
phone varchar(30)
);
为 name 字段添加唯一约束:
alter table customers add constraint name_uk unique(name);
Customer实体类
在domain包下封装一个实体类
package mvcapp.domain;
/**
* Created by japson on 12/24/2017.
*/
public class Cusromer {
private Integer id;
private String name;
private String address;
private String phone;
public Integer getId() { return id; }
public void setId(Integer id) { this.id = 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 "Cusromer{" +
"id=" + id +
", name='" + name + '\'' +
", address='" + address + '\'' +
", phone='" + phone + '\'' +
'}';}
}
JDBCUtils
JDBC操作的工具类,使用c3p0数据库连接池。有创建连接和释放连接的方法。我们需要导入两个jar包:c3p0-0.9.5.2.jar
和mchange-commons-java-0.2.11.jar
。并且在src文件夹下建立c3p0-config.xml
配置文件(参见数据库连接池)。
我们通过c3p0数据源来实现JDBCUtils工具类
package mvcapp.db;
/**
* Created by japson on 12/24/2017.
*/
import com.mchange.v2.c3p0.ComboPooledDataSource;
import javax.sql.DataSource;
import java.beans.PropertyVetoException;
import java.sql.Connection;
import java.sql.SQLException;
/**
* JDBC操作的工具类
*/
public class JDBCUtils {
/**
* 指定数据源为c3p0的配置文件
*/
private static DataSource dataSource = null;
static {
dataSource = new ComboPooledDataSource("mvcapp");
}
/**
* 返回数据源的一个Connection对象
* @return
*/
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
* 关闭Connection连接
* @param connection
*/
public static void releaseConnection(Connection connection) {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
DAO类
DAO是一个数据访问对象。创建了一个DAO类,该类封装了基本的对数据库进行增删改查的操作方法,以供子类继承用。当前的DAO没有事务,直接在方法中获取数据库连接。
该类的参数是T,当前DAO处理的实体类的类型是什么。使用反射技术进行解决。详见DBUtils编写DAO
整个的DAO类采取DBUtils解决方案。导入DBUtils的jar包
package mvcapp.dao;
/**
* Created by japson on 12/24/2017.
*/
import mvcapp.db.JDBCUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
/**
* 封装了基本的CRUD的方法,以供子类继承使用
* 当前DAO没有事务,直接在方法中获取数据库连接。
* 整个DAO采取DBUtils解决方案
* @param <T> 当前DAO处理的实体类的类型是什么
*/
public class DAO<T> {
//DBUtils的Queryrunner对象
private QueryRunner queryRunner = new QueryRunner();
//用来表示泛型的类型
public Class<T> clazz;
//在构造函数中以反射的方式
public DAO() {
//获取此Class所表示的实体的父类的泛型参数类型
Type superClass = getClass().getGenericSuperclass();
if (superClass instanceof ParameterizedType) {
//将父类的泛型参数类型,强转为参数化类型
ParameterizedType parameterizedType = (ParameterizedType) superClass;
//获取参数化类型的数组
Type[] typeArgs = parameterizedType.getActualTypeArguments();
if (typeArgs[0] instanceof Class) {
//取数组的第一个值为需要传入泛型的类型
clazz = (Class<T>) typeArgs[0];
}
}
}
/**
* 该方法封装了INSERT,DELETE,UPDATE操作
* @param sql SQL语句
* @param args 填充SQL语句的占位符
*/
public void update(String sql,Object... args) {
Connection connection = null;
try {
connection = JDBCUtils.getConnection();
queryRunner.update(connection,sql,args);
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtils.releaseConnection(connection);
}
}
/**
* 返回对应的T的一个实体类的对象
*/
public T get(String sql,Object... args) {
Connection connection = null;
try {
connection = JDBCUtils.getConnection();
return queryRunner.query(connection,sql, new BeanHandler<T>(clazz),args);
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtils.releaseConnection(connection);
}
return null;
}
/**
* 返回T所对应的List
*/
public List<T> getForList(String sql,Object... args) {
Connection connection = null;
try {
connection = JDBCUtils.getConnection();
return queryRunner.query(connection,sql, new BeanListHandler<T>(clazz),args);
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtils.releaseConnection(connection);
}
return null;
}
/**
* 返回一个字段的值,例如返回某一条记录的customerName,或返回数据表中有多少条记录等
*/
public <E> E getForValue(String sql,Object... args) {
Connection connection = null;
try {
connection = JDBCUtils.getConnection();
return (E) queryRunner.query(connection,sql, new ScalarHandler(),args);
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtils.releaseConnection(connection);
}
return null;
}
}
CustomerDAO接口
CustomerDAO接口中的方法取决于业务的具体方法:
1. 显式数据库,获取数据库所有的信息,即返回一个Customer的List
2. 添加一个新的Customer并保存
3. 更新一条数据的时候需要先根据id将该数据查询并显示出来
4. 删除数据,根据id
5. 在添加的时候,要看跟要添加的数据的name一样的数据有几个,如果不是0个,则不能添加
package mvcapp.dao;
import mvcapp.domain.Customer;
import java.util.List;
/**
* Created by japson on 12/24/2017.
*/
public interface CustomerDAO {
List<Customer> getAll();
void save(Customer cusromer);
Customer get(Integer id);
void delete(Integer id);
/**
* 返回和name相等的记录
* @param name
* @return
*/
long getCountWithName(String name);
void update(Customer customer);
}
接口的实现类CustomerDAOJDBCImpl
继承DAO<Cusromer>
类实现CustomerDAO接口
package mvcapp.dao.impl;
import mvcapp.dao.CustomerDAO;
import mvcapp.dao.DAO;
import mvcapp.domain.Customer;
import java.util.List;
public class CustomerDAOJDBCImpl extends DAO<Customer> implements CustomerDAO {
@Override
public List<Customer> getAll() {
String sql = "SELECT id,NAME,address,phone FROM webcustomers";
return getForList(sql);
}
@Override
public void save(Customer cusromer) {
String sql = "INSERT INTO webcustomers(NAME,address,phone) VALUES(?,?,?)";
update(sql,cusromer.getName(),cusromer.getAddress(),cusromer.getPhone());
}
@Override
public Customer get(Integer id) {
String sql = "SELECT id,name,address,phone FROM webcustomers WHERE id = ?";
return get(sql,id);
}
@Override
public void delete(Integer id) {
String sql = "DELETE FROM webcustomers WHERE id = ?";
update(sql,id);
}
@Override
public long getCountWithName(String name) {
String sql = "SELECT COUNT(id) FROM webcustomers WHERE name = ?";
return getForValue(sql,name);
}
@Override
public void update(Customer customer) {
String sql = "UPDATE webcustomers SET name = ?,address = ?,phone = ? WHERE id = ?";
update(sql,customer.getName(),customer.getAddress(),customer.getPhone(),customer.getId());
}
}
技术问题:多个请求使用同一个Servlet
我们要提出一个技术问题:如何多个请求使用同一个Servlet。
假设我们现在有三种请求操作:添加请求,查询请求,删除请求。如果每一个请求操作对应一个Servlet,那么太繁琐了。而如果我们要使用同一个Servlet,那么我们还需要区分三种操作的不同。那么我们首先提出一种方法:
在路径中添加request参数
- 获取method请求参数的值
- 根据method的值调用对应的方法
首先我们对应三种请求方式,给出一个连接下的三个不同的参数:
<a href="customerServlet?method=add">add</a>
<a href="customerServlet?method=query">query</a>
<a href="customerServlet?method=delete">delete</a>
然后在CustomerServlet中:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String method = request.getParameter("method");
switch (method) {
case "add" : add(request,response); break;
case "query" : query(request,response); break;
case "delete" : delete(request,response); break;
}
}
这样我们就可以实现了,但是这个方法还有一些不足:
1. 当添加一个请求时,需要在Servlet中修改两处代码
2. url中使用method=xxx的方法暴露了要调用的方法,不私密
更好的解决方案
我们要给出一个更通用的方案,对于url中暴露的参数和方法,我们使用一种加扩展名的servletPath。跳转到一个通用的Servlet中然后我们对servletPath进行处理,得到方法名。然后利用反射,通过方法名来获得对应的方法。具体方法如下:
- 获取servletPath:
/addCustomer.do
(自定义一个扩展名) - 更改servlet的urlPatterns为
*.do
,该映射可以接收一切以.do
结尾的请求 - 去除/和.do,得到方法名
- 利用反射调用servletPath对应的方法
- 创建对应的方法
具体代码如下:
<a href="addCustomer.do">add</a>
<a href="query.do">query</a>
<a href="deleteCustomer.do">delete</a>
可见:在点击之后会转入到addCustomer.do
这样的连接。我们就要在servlet的注解中更改servlet的urlPatterns:
@WebServlet(name = "CustomerServlet",urlPatterns = "*.do")
这样,只要我们点击带有.do
拓展名的路径,就会转到这用一个servlet中。
然后我们进行如下的操作:
1. 通过getServletPath()
方法获取ServletPath: /xxxx.do
形式
2. 通过substring取出/
和.do
,得到xxxx
字符串
3. 利用反射,通过指定方法名,即字符串xxxx
,获取叫这个名的方法,为Method类型的
4. 利用反射调用对应的方法
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取ServletPath: /xxxx.do 形式
String servletPath = request.getServletPath();
//取出/和.do,得到xxxxx字符串
String methodName = servletPath.substring(1);
methodName = methodName.substring(0,methodName.length()-3);
System.out.println(methodName);
//利用反射,通过字符串xxxx,获取叫这个名的方法
try {
//getMethod()方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象。
Method method = getClass().getDeclaredMethod(methodName,HttpServletRequest.class,HttpServletResponse.class);
//利用反射调用对应的方法
//可以在不知道具体类的情况下,根据配置字符串去调用一个类的方法。
method.invoke(this,request,response);
} catch (Exception e) {
e.printStackTrace();
}
}
Servlet之查询操作
查询整张表
查询操作涉及了MVC的整个的流程query.do --> doPost --> query -->JSP
通过上面的方法,我们将各种操作请求对应成了一个Servlet,因此我们可以在Servlet中写具体的方法。其中所有的操作都是基于DAO实现的:
private CustomerDAO customerDAO = new CustomerDAOJDBCImpl();
Servlet中的query()方法的操作步骤如下:
- 调用CustomerDAO的getAll()方法得到Customer的集合
- 把Customer的集合作为request的属性
- 转发页面到index.jsp(不能使用重定向)
private void query(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 调用CustomerDAO的getAll()方法,得到Customer的集合
List<Customer> customers = customerDAO.getAll();
//2. 把Customer的集合放入request中
request.setAttribute("customers",customers);
//3. 转发页面到index.jsp
request.getRequestDispatcher("/index.jsp").forward(request,response);
}
在JSP中的操作为:
1. 获取request中的customer属性
2. 遍历显示
模糊查询
我们下面要把查询操作变为模糊查询,即:仅输入部分条件即可进行,而之前的查询整张表的操作可以看作是模糊查询中对的无条件查询。
根据传入的name,address,phone进行模糊查询。其关键在于SQL语句使用条件查询。举一个例子: name: a、address: b、phone: 3 则 SQL 语句的样子为: SELECT id, name, address, phone FROM customers WHERE name LIKE ‘%a%’ AND address LIKE ‘%b%’ AND phone LIKE ‘%3%’
那么我们具体写代码的时候,我们从底层DAO开始考虑。目前有三个DAO,一个是DAO类,封装了基本操作;一个是CustomerDAO接口,是提供了业务相关的操作的声明;CustomerDAOJDBCImpl是一个实现类,继承了DAO类实现了CustomerDAO方法。因此我们应该在CustomerDAO接口中声明模糊查询的方法,然后在CustomerDAOJDBCImpl类中实现它。
但是在此之前我们要创建一个CriteriaCustomer类,用来封装查询条件,也就是name,address等属性。那么我们为什么要创建一个新的类,而不用之前的domain类呢?因为CriteriaCustomer类是专门为查询条件而服务的:我们在模糊查询的时候,有时不仅是对实体对象的属性进行查询,还要对属性的某一个区间进行查询。例如我们在购买手机时,除了品牌等属性,还有会价格区间,这是和domain中的属性不同的,因此需要一个单独的类。
我们在CustomerDAO中封装了查询条件:
//返回满足查询条件的List
List<Customer> getForListWithCriteriaCustomer(CriteriaCustomer criteriaCustomer);
通过上面的一个例子我们知道查询语句是这样的:SELECT id, name, address, phone FROM customers WHERE name LIKE ‘%a%’ AND address LIKE ‘%b%’ AND phone LIKE ‘%3%’
,那么我们从页面获取的信息是a,b,3,这样的,我们还需要将其拼接%a%
。我们可以把这一步放到CriteriaCustomer类的Getter方法中操作:
public String getName() {
if (name == null)
name = "%%";
else
name = "%" + name + "%";
return name;
}
这样我们就可以直接在实现方法中使用getName()方法而不需要进行拼接%%
了:
public List<Customer> getForListWithCriteriaCustomer(CriteriaCustomer criteriaCustomer) {
String sql = "SELECT id,name,address,phone FROM webcustomers WHERE " +
"name LIKE ? AND address LIKE ?";
//在CriteriaCustomer中的getter方法中返回带有"%%"的字符串
return getForList(sql,criteriaCustomer.getName(),criteriaCustomer.getAddress(),criteriaCustomer.getPhone());
}
之后我们需要修改Servlet:
private void query(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取模糊查询的请求参数
String name = request.getParameter("name");
String address = request.getParameter("address");
String phone = request.getParameter("phone");
//把请求参数封装为一个CriteriaCustomer对象
CriteriaCustomer criteriaCustomer = new CriteriaCustomer(name,address,phone);
//1. 调用CustomerDAO的getForListWithCriteriaCustomer()方法,参数为封装了请求参数的对象,得到Customer的集合
List<Customer> customers = customerDAO.getForListWithCriteriaCustomer(criteriaCustomer);
//2. 把Customer的集合放入request中
request.setAttribute("customers",customers);
//3. 转发页面到index.jsp
request.getRequestDispatcher("/index.jsp").forward(request,response);
}