MVC模式
首先MVC模式并不是javaweb项目中独有的,MVC是一种软件工程中的一种设计模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller),即为MVC。
它是一种软件设计的典范,最早为Trygve Reenskaug提出,为施乐帕罗奥多研究中心(Xerox PARC)的Smalltalk语言发明的一种软件设计模式。
- MVC模式详解:
虽然MVC并不是Java当中独有的,但是现在几乎所有的B/S的架构都采用了MVC框架模式。
控制器Controller:控制器即是控制请求的处理逻辑,对请求进行处理,负责请求转发和重定向;
视图View:视图即是用户看到并与之交互的界面,比如HTML(静态资源),JSP(动态资源)等等。
模型Model:模型代表着一种企业规范,就是业务流程/状态的处理以及业务规则的规定。
业务流程的处理过程对其他层来说是不透明的,模型接受的请求,并返回最终的处理结果。
业务模型的设计可以说是MVC的核心。
- MVC高级框架应用:
MVC模式被广泛用于Java的各种框架中,比如Struts2、springMVC等等都用到了这种思想。
Struts2是基于MVC的轻量级的web应用框架。基于MVC,说明基于Struts2开发的Web应用自然就能实现MVC,也说明Struts2着力于在MVC的各个部分为我们的开发提供相应帮助。
JSP开发模型
JavaWeb经历两个时期:JSP Model1 和 JSP Model2
- JSP Model1:
JSP Model1是JavaWeb早期的模型,它适合小型Web项目,开发成本低!Model1第一代时期,服务器端只有JSP页面,所有的操作都在JSP页面中,连访问数据库的API也在JSP页面中完成。也就是说,所有的东西都耦合在一起,对后期的维护和扩展极为不利。
- JSP Model1的优化(Model1第二代):
JSP Model1优化后有所改进,把业务逻辑和数据访问的内容放到了JavaBean(狭义JavaBean:实体类,广义JavaBean:实体类,dao,service,工具类)中,而JSP页面负责显示以及请求调度的工作。虽然第二代比第一代好了些,但还让JSP做了过多的工作,JSP中把视图工作和请求调度(控制器)的工作耦合在一起了。
- JSP Model2:
JSP Model2模式已经可以清晰的看到MVC完整的结构了。
JSP:视图层,用来与用户打交道。负责接收数据,以及显示数据给用户;
Servlet:控制层,负责找到合适的模型对象来处理业务逻辑,转发到合适的视图;
JavaBean:模型层,完成具体的业务工作,例如:开启、转账等。
这就是javaweb经历的两个时期,JSP Model2适合多人合作开发大型的Web项目,各司其职,互不干涉,有利于开发中的分工,有利于组件的重用。但是,Web项目的开发难度加大,同时对开发人员的技术要求也提高了。
基于MVC的三层架构
虽然MVC把程序分成三部分,每个部分负责不同的功能,但是这只是逻辑的分离,实际代码并没有真正分离,特别是Model(包括业务、数据访问和实体类、工具类等)部分的代码,为了增强代码的维护性和降低代码耦合性,需要把代码分层管理,于是就有了三层架构:
分别是:web层(表示|界面层)、service层(业务逻辑层)、dao层(数据访问层、持久层)
web层对应MVC中的Servlet和JSP,其他层都属于MVC中的Model。
MVC事务案例
- 转账页面:
<body>
<form action="${pageContext.request.contextPath }/servlet/AccountServlet" method="post" >
转出方:<input type="text" name="outaccount"/><br/>
转入方:<input type="text" name="intaccount"/><br/>
金额<input type="text" name="money"/><br/>
<input type="submit" value="转账"/><br/>
</form>
</body>
- 数据库配置文件:
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/gp03
username=root
password=root
initialSize=10
maxActive=50
maxWait=50000
minIdle=5
- 项目分包:
com.qf.domain、com.qf.dao、com.qf.dao.impl
com.qf.service、com.qf.service.impl
com.qf.utils
com.qf.web.servlet
- 工具类:
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class DataSourceUtils {
private static DruidDataSource dataSource;
//线程局部变量
private static ThreadLocal<Connection> threadLocal;
static {
try {
threadLocal = new ThreadLocal<>();
Properties properties = new Properties();
InputStream is = DataSourceUtils.class.getClassLoader().getResourceAsStream("database.properties");
properties.load(is);
dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
System.out.println("初始化连接池失败");
}
}
public static DataSource getDataSource() {
return dataSource;
}
//获取连接
public static Connection getConnection() throws SQLException {
Connection connection = threadLocal.get();
if (connection == null) {
connection = getDataSource().getConnection();
threadLocal.set(connection);
}
return connection;
}
//开启事务
public static void startTransaction() throws SQLException {
Connection connection = getConnection();
connection.setAutoCommit(false);
}
//提交
public static void commit() throws SQLException {
Connection connection = getConnection();
connection.commit();
}
//回滚
public static void rollback() throws SQLException {
Connection connection = getConnection();
connection.rollback();
connection.commit();
}
//关闭
public static void close() throws SQLException {
Connection connection = getConnection();
connection.close();
threadLocal.remove();
}
}
- DAO:
@Data
public class Account {
private int id;
private String name;
private BigDecimal money;
}
public interface AccountDao {
//加钱
public void addMoney(Account account, BigDecimal m);
//扣钱
public void subMoney(Account account,BigDecimal m);
}
public class AccountDaoImpl implements AccountDao {
@Override
public void addMoney(Account account, BigDecimal m) {
QueryRunner qr = new QueryRunner();
try {
qr.update(DataSourceUtils.getConnection(), "update account set money=money+? where id=?", m, account.getId());
} catch (SQLException e) {
throw new RuntimeException("加钱失败", e);
}
}
@Override
public void subMoney(Account account, BigDecimal m) {
QueryRunner qr = new QueryRunner();
try {
qr.update(DataSourceUtils.getConnection(), "update account set money=money-? where id=?", m, account.getId());
} catch (SQLException e) {
throw new RuntimeException("扣钱失败", e);
}
}
}
- Service:
public interface AccountService {
//转账
public void transMoney(Account from, Account to, BigDecimal m);
}
public class AccountServiceImpl implements AccountService {
@Override
public void transMoney(Account from, Account to, BigDecimal m) {
try {
//开启事务
DataSourceUtils.startTransaction();
AccountDao accountDao = new AccountDaoImpl();
//1 先把from钱扣掉
accountDao.subMoney(from, m);
//int i=10/0;
//2 再给to加钱
accountDao.addMoney(to, m);
//提交
DataSourceUtils.commit();
} catch (Exception e) {
//回滚
try {
DataSourceUtils.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
throw new RuntimeException("转账失败", e);
} finally {
try {
DataSourceUtils.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
- Servlet:
@WebServlet(name = "TransServlet", urlPatterns = "/trans")
public class TransServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
//接受数据
String from = request.getParameter("from");
String to = request.getParameter("to");
String money = request.getParameter("money");
//验证金额
BigDecimal m = new BigDecimal(money);
if (m.doubleValue() <= 0) {
response.getWriter().write("转账金额不能小于0");
return;
}
//转账
AccountService accountService = new AccountServiceImpl();
Account accfrom = new Account(Integer.parseInt(from), "宁宁", new BigDecimal(0));
Account accto = new Account(Integer.parseInt(to), "春春", new BigDecimal(0));
try {
accountService.transMoney(accfrom, accto, m);
response.getWriter().write("转账成功");
} catch (Exception e) {
e.printStackTrace();
response.getWriter().write("转账失败");
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
}
- 防止表单重复提交:
常见用两种方式
1、通过JavaScript屏蔽提交按钮(不推荐)
通过js代码:当用户点击提交按钮后,屏蔽提交按钮使用户无法点击提交按钮或点击无效,
从而实现防止表单重复提交。
缺点:js代码很容易被绕过。
比如用户通过刷新页面方式,
或使用postman等工具绕过前段页面仍能重复提交表单。因此不推荐此方法。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>转账</title>
<script type="text/javascript">
//默认提交状态为false
var commitStatus = false;
function dosubmit(){
if(commitStatus==false){
//提交表单后,将提交状态改为true
commitStatus = true;
document.getElementById("btnsubmit").disabled=true;
return true;
}else{
return false;
}
}
</script>
</head>
<body>
<h2>转账</h2>
<form action="${pageContext.request.contextPath}/trans" onsubmit="return dosubmit()" method="post" target="_blank">
<input type="text" name="from" placeholder="请输入转出账号"><br/>
<input type="text" name="to" placeholder="请输入转入账号"><br/>
<input type="text" name="money" placeholder="请输入金额"><br/>
<input id="btnsubmit" type="submit" value="转账">
</form>
</body>
</html>
2、利用Session防止表单重复提交(推荐)
服务器返回表单页面时,会先生成一个subToken保存于session,并把该subToken传给表单页面。
当表单提交时会带上subToken,服务器判断session中的subToken和表单提交subToken是否一致。
若不一致或session的subToken为空或表单未携带subToken则不通过。
首次提交表单时session的subToken与表单携带的subToken一致走正常流程,
然后服务器会删除session保存的subToken。
当再次提交表单时由于session的subToken为空则不通过。从而实现了防止表单重复提交。
public class TokenProccessor {
private TokenProccessor() {}
private static final TokenProccessor instance = new TokenProccessor();
public static TokenProccessor getInstance() {return instance;}
public String makeToken() {
String str = System.currentTimeMillis() + new Random().nextInt(999999999) + "";
try {
//1使用md5加密
MessageDigest digest = MessageDigest.getInstance("md5");
digest.update(str.getBytes());
byte[] digest1 = digest.digest();
//2使用base64
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(digest1);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
}
public class TokenTools {
//把token放入session
public static void createToken(HttpServletRequest request, String tokenkey) {
String token = TokenProccessor.getInstance().makeToken();
request.getSession().setAttribute(tokenkey, token);
}
//把token从session删除
public static void removeToken(HttpServletRequest request, String tokenkey) {
request.getSession().removeAttribute(tokenkey);
}
}
@WebServlet(name = "TransViewServlet", urlPatterns = "/transview")
public class TransViewServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//生成一个token
TokenTools.createToken(request, "mytoken");
request.getRequestDispatcher("/WEB-INF/jsp/transview.jsp").forward(request, response);
}
}
<body>
<h2>转账</h2>
<form action="${pageContext.request.contextPath}/trans" onsubmit="return dosubmit()" method="post" target="_blank">
<input type="text" name="from" placeholder="请输入转出账号"><br/>
<input type="text" name="to" placeholder="请输入转入账号"><br/>
<input type="text" name="money" placeholder="请输入金额"><br/>
<input type="hidden" name="mytoken" value="${mytoken}">
<input id="btnsubmit" type="submit" value="转账">
</form>
</body>
@WebServlet(name = "TransServlet", urlPatterns = "/trans")
public class TransServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
//判断是否是重复提交
boolean b = isRepeatSubmit(request, "mytoken");
if (b) {
response.getWriter().write("不好意思,重复提交了...");
return;
}
//通过验证删除
TokenTools.removeToken(request, "mytoken");
//接受数据
String from = request.getParameter("from");
String to = request.getParameter("to");
String money = request.getParameter("money");
//验证金额
BigDecimal m = new BigDecimal(money);
if (m.doubleValue() <= 0) {
response.getWriter().write("转账金额不能小于0");
return;
}
//转账
AccountService accountService = new AccountServiceImpl();
Account accfrom = new Account(Integer.parseInt(from), "宁宁", new BigDecimal(0));
Account accto = new Account(Integer.parseInt(to), "春春", new BigDecimal(0));
try {
accountService.transMoney(accfrom, accto, m);
response.getWriter().write("转账成功");
} catch (Exception e) {
e.printStackTrace();
response.getWriter().write("转账失败");
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
public boolean isRepeatSubmit(HttpServletRequest request, String clientTokeyKey) {
//1 判断session有没有
String serverToken = (String) request.getSession().getAttribute(clientTokeyKey);
if (serverToken == null) {
return true;
}
//2 判断客户端有没有token
String clientToken = request.getParameter(clientTokeyKey);
if (clientToken == null) {
return true;
}
//3 两个是否相同
if (!serverToken.equals(clientToken)) {
return true;
}
return false;
}
}
MVC分页案例
分页是web应用程序非常重要的一个技术。数据库中的数据可能是成千上万的,不可能把这么多的数据一次显示在浏览器上面。一般根据每行数据在页面上所占的空间每页显示若干行,比如一般20行是一个比较理想的显示状态。
分页的思路:对于海量的数据查询,需要多少就取多少,显然是最佳的解决方法,
假如某个表中有200万条记录,第一页取前20条,第二页取21~40条记录。
select * from 表名 order by id limit 0,20;
select * from 表名 order by id limit 20,20;
select * from 表名 order by id limit 40,20;
- 数据库配置文件:
#连接设置
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/myschool
username=root
password=root
#<!-- 初始化连接 -->
initialSize=10
#最大连接数量
maxActive=50
#<!-- 最小空闲连接 -->
minIdle=5
#<!-- 超时等待时间以毫秒为单位 6000毫秒/1000等于60秒 -->
maxWait=5000<?xml version="1.0" encoding="UTF-8"?>
- 实体类和工具类:
@Data
public class Student {
private int studentNo;
private String loginPwd;
private String studentName;
private String sex;
private Date bornDate;
}
@Setter
@Getter
public class PageBean<T> {
//五个属性
private int pageIndex;
private int pageSize;
private long totalSize;
private int pageCount;
private List<T> data;
//开始和结束页码
private int startPage;
private int endPage;
public PageBean(int pageIndex, int pageSize, long totalSize, List<T> data) {
this.pageIndex = pageIndex;
this.pageSize = pageSize;
this.totalSize = totalSize;
this.data = data;
//计算pageCount
pageCount = (int) (totalSize % pageSize == 0 ? totalSize / pageSize : totalSize / pageSize + 1);
//设置开始页码和结束页码
//1正常情况
startPage = pageIndex - 4;
endPage = pageIndex + 5;
//2当前页面小于5
if (pageIndex < 5) {
startPage = 1;
endPage = 10;
}
//3当前页面大于总页码-5
if (pageIndex > pageCount - 5) {
startPage = pageCount - 9;
endPage = pageCount;
}
//4总页码小于10
if (pageCount <= 10) {
startPage = 1;
endPage = pageCount;
}
}
}
public class DataSourceUtils {
private static DruidDataSource dataSource;
//线程局部变量
private static ThreadLocal<Connection> threadLocal;
static {
try {
threadLocal=new ThreadLocal<>();
Properties properties=new Properties();
InputStream is= DataSourceUtils.class.getClassLoader().getResourceAsStream("database.properties");
properties.load(is);
dataSource= (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
System.out.println("初始化连接池失败");
}
}
public static DataSource getDataSource(){
return dataSource;
}
//获取连接
public static Connection getConnection() throws SQLException {
Connection connection = threadLocal.get();
if(connection==null){
connection=getDataSource().getConnection();
threadLocal.set(connection);
}
return connection;
}
}
- DAO:
public interface StudentDao {
//分页查询方法
public List<Student> findByPage(int pageIndex, int pageSize);
//添加
public void add(Student stu);
//获取数据个数的方法
public long getCount();
}
public class StudentDaoImpl implements StudentDao {
@Override
public List<Student> findByPage(int pageIndex, int pageSize) { //1 20 2 20
QueryRunner qr = new QueryRunner(DataSourceUtils.getDataSource());
try {
return qr.query("select * from student order by studentNo limit ?,?", new BeanListHandler<>(Student.class), (pageIndex - 1) * pageSize, pageSize);
} catch (Exception e) {
throw new RuntimeException("分页查询失败", e);
}
}
@Override
public void add(Student stu) {
QueryRunner qr = new QueryRunner(DataSourceUtils.getDataSource());
try {
qr.update("insert into student(studentNo,loginPwd,studentName,sex,bornDate) values(?,?,?,?,?);", stu.getStudentNo(), stu.getLoginPwd(), stu.getStudentName(), stu.getSex(), stu.getBornDate());
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("添加失败", e);
}
}
@Override
public long getCount() {
QueryRunner qr = new QueryRunner(DataSourceUtils.getDataSource());
try {
return qr.query("select count(*) from student", new ScalarHandler<>());
} catch (Exception e) {
throw new RuntimeException("获取数据个数失败", e);
}
}
}
- Service:
public interface StudentService {
PageBean<Student> getDataByPage(int pageIndex, int pageSize);
}
public class StudentServiceImpl implements StudentService {
@Override
public PageBean<Student> getDataByPage(int pageIndex, int pageSize) {
//1获取数据
StudentDao studentDao=new StudentDaoImpl();
List<Student> data=studentDao.findByPage(pageIndex, pageSize);
//2获取数据个数
long totalSize = studentDao.getCount();
PageBean<Student> pageBean=new PageBean<>(pageIndex, pageSize, totalSize, data);
return pageBean;
}
}
- Servlet:
@WebServlet(name = "StudentListServlet",urlPatterns = "/stulist")
public class StudentListServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
//1获取pageIndex,pageSize;
String spageIndex = request.getParameter("pageIndex");
String spageSize = request.getParameter("pageSize");
int pageIndex=1;
int pageSize=10;
if(!StringUtils.isEmpty(spageIndex)){
pageIndex=Integer.parseInt(spageIndex);
}
if(!StringUtils.isEmpty(spageSize)){
pageSize=Integer.parseInt(spageSize);
}
//分页查询
StudentService studentService=new StudentServiceImpl();
PageBean<Student> pageBean=studentService.getDataByPage(pageIndex,pageSize);
if(pageBean!=null){
request.setAttribute("pageBean", pageBean);
}else{
request.setAttribute("msg", "数据为空");
}
request.getRequestDispatcher("/studentlist.jsp").forward(request, response);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
}
- jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>学生列表</title>
</head>
<body>
<h2>学生信息</h2>
${msg}
<table>
<tr>
<th>学号</th>
<th>密码</th>
<th>姓名</th>
<th>性别</th>
<th>出生日期</th>
</tr>
<c:forEach items="${pageBean.data}" var="stu">
<tr>
<td>${stu.studentNo}</td>
<td>${stu.loginPwd}</td>
<td>${stu.studentName}</td>
<td>${stu.sex}</td>
<td>${stu.bornDate}</td>
</tr>
</c:forEach>
</table>
<div id="nav">
<a href="${pageContext.request.contextPath}/stulist?pageIndex=1&pageSize=${pageBean.pageSize}">首页</a>
<c:if test="${pageBean.pageIndex>1}">
<a href="${pageContext.request.contextPath}/stulist?pageIndex=${pageBean.pageIndex-1}&pageSize=${pageBean.pageSize}">上一页</a>
</c:if>
<c:forEach begin="${pageBean.startPage}" end="${pageBean.endPage}" var="n">
<a href="${pageContext.request.contextPath}/stulist?pageIndex=${n}&pageSize=${pageBean.pageSize}">${n}</a>
</c:forEach>
<c:if test="${pageBean.pageIndex<pageBean.pageCount}">
<a href="${pageContext.request.contextPath}/stulist?pageIndex=${pageBean.pageIndex+1}&pageSize=${pageBean.pageSize}">下一页</a>
</c:if>
<a href="${pageContext.request.contextPath}/stulist?pageIndex=${pageBean.pageCount}&pageSize=${pageBean.pageSize}">尾页</a>
共【${pageBean.pageIndex}/${pageBean.pageCount}】页
<input id="pagenum" type="number" name="pagenum" style="width: 50px" value="${pageBean.pageIndex}"><input type="button" value="跳转" onclick="changenum()">
</div>
<script type="text/javascript">
function changenum() {
var pnum= document.getElementById("pagenum").value;
if(isNaN(pnum)){
alert("您输入数字有误");
return;
}
window.location='${pageContext.request.contextPath}/stulist?pageIndex='+pnum+'&pageSize=${pageBean.pageSize}';
}
</script>
</body>
</html>