1. 程式人生 > >MVC的實用教程,帶你理解它的世界

MVC的實用教程,帶你理解它的世界

之前看過我部落格的朋友應該都已經懂了servlet和JSP的使用和他們的特性,我們在使用servlet進行設計開發時,會遇到短板,我們需要在後臺程式碼中寫入大量的html內容,內容繁瑣而且也不好看更不方便修改和維護。我們在使用JSP進行開發的時候,後臺業務邏輯程式碼又帶到了前臺頁面中去寫,很不方便。那MVC的這種設計模式就解決了這個問題,它能夠讓我們的前後端不在一起寫,頁面就是頁面,後臺就是後臺,我們只需要完成前後端資料的互動就OK了。那很多人會納悶,資料怎麼傳遞的呢?彆著急,在下面的demo中我會很詳細的解釋。

技術就是這樣,單一的特性很重要,但是隻有把他們的單一特性結合起來使用,更方便我們的開發,這才是更重要的,其實這也就是所謂框架的理念。

MVC(Model View Control)是一種分層設計模式,所謂分層,就是誰該幹嘛就幹嘛,互不打擾。

Model指的是模型,也就是我們通常理解的資料準備層,它主要負責連線資料庫,準備sql並執行,完成我們的增刪改查操作。

View指的是檢視,也就是我們常說的前端頁面,用來展示後臺我們需要的資料和內容。

Control指的是控制層,他是MVC的核心部分,連通資料層和展示層。它怎麼控制呢,首先它會從資料準備層調取相應的方法,獲取到資料到他這裡,然後把這些資料進行業務處理,再通過傳遞引數或者request物件等將資料傳遞到前端頁面。或者它把前端頁面傳遞過來的資料通過呼叫制定的方法交給資料準備層,讓他存進資料庫中。這就是他的工作內容。

其實MVC的這種設計方式很容易理解,理解懂了MVC,相信大家在以後理解更多框架的時候會有很大的幫助。

下面我通過自己搭建的demo給大家逐步分析MVC是怎樣從無到有的。

一:準備工作

1. 首先準備我們的資料庫,我這裡的demo是通過mvc完成對一張表的增刪改查操作。所以只新建一張表,表結構如下圖:

主鍵id(自增長)、英雄名name、血量Hp、傷害damage

 手動新增幾條資料進去。新建資料庫和新增資料就不一一贅述了。

2.eclipse中新建一個dynamic Web project專案,配置好tomcat,新建web.xml檔案。在lib路徑下匯入jar包,這裡用到的jar包有:mysql-conncetor-java-bin.jar,servlet-api.jar,jstl.jar,standard.jar。專案路徑截圖如下:

3. 在web下新建heroList.jsp用來展示我們查詢到的結果。src路徑下新建bean包(javabean也就是我們的實體類)、dao包(連線資料庫並執行sql,也就是model層),這裡的default package包下就是我們的control層,在src下右擊新建class,把預設的報名刪掉就會新建在這個 default package包中。

二:開始設計

我們先實現一個功能,從資料庫中查詢出所有的英雄,並顯示在前端頁面的表格中。

1. 首先新建我們的bean包中javaBean實體類,Hero.java,定義變數id,name,hp,damage。並給出對應的getter和setter方法。實體類準備好了。(如果不明白為什麼要建實體類的話,你就把它理解成一個物件,一個物件裡面裝的就是我們資料庫中的一條記錄就好了,配合百度你就懂了)

2. 在dao包中新建HeroDao.java,完成JDBC連線資料庫,完成查詢資料庫中所有英雄的方法。程式碼如下:

package com.mvc.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import com.mvc.bean.Hero;

public class HeroDao {
	
	public HeroDao() {
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
	
	public Connection getConnection() {
		String user = "root";
		String password = "root";
		String data = "jdbc:mysql://localhost:3306/test";
		Connection con = null;
		try {
			con = DriverManager.getConnection(data, user, password);
		} catch (SQLException e) {
			e.printStackTrace();
		}
		System.out.println("*****************資料庫連線成功!*****************");
		return con;
	}
	
	public List<Hero> getList() {
		List<Hero> listHero = new ArrayList<Hero>();
		try {
			Connection con = getConnection();
			Statement stmt = con.createStatement();
			String sql = "select * from hero";
			ResultSet rs = stmt.executeQuery(sql);
			while(rs.next()) {
				Hero hero = new Hero();
				int id = rs.getInt("id");
				String name = rs.getString("name");
				int damage = rs.getInt("damage");
				float hp = rs.getFloat("hp");
				hero.setId(id);
				hero.setName(name);
				hero.setHp(hp);
				hero.setDamage(damage);
				listHero.add(hero);
			}
			stmt.close();
			con.close();
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		return listHero;
	}
}

大概解釋一下:重寫heroDao的無參構造方法,並在其中載入JDBCMySQL的載入類。

新建getConnection方法,用來連線資料庫,我們在其他方法中直接呼叫即可 。

getList()方法,獲取資料庫中所有的英雄,此為JDBC基礎知識,如果看不懂就狂補一下JDBC的基礎吧。

也就是說,如果我們在其他類中呼叫了getList()方法之後,返回值中的listHero集合中就是我們資料庫中的全部英雄資料。

3. 編寫我們的control層,連通前後端。在src下新建類HeroList.java,預設包刪掉(主要是方便在web.xml中配置servlet簡便,不用加包名)。這個類就是一個servlet,讓他繼承httpServlet類。並重寫service方法,用來處理請求。在service方法中,我們新建hero實體,並新建一個集合heros來存放資料庫中查詢出來的集合。然後呼叫heroDao中的getList()方法。將返回結果給heros。將heros存在在session中,重點來了,MVC控制層在前後端交換資料的方式就是通過request或者session物件的setAttribute()方法來存放和拿取資料。這裡再解釋一下,session會話物件在使用者開發瀏覽器,訪問伺服器的時候就自動建立了,關閉瀏覽器或者超過有效期就失效了。request物件傳遞引數只會在一次完整的請求中有效,請求結束或者發起新的請求時資料就丟失了。一般使用session存放資料的多。具體請參考我的部落格JSP和servlet中對session的介紹。最後將伺服器重定向到heroList.jsp頁面中。程式碼如下:

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.mvc.bean.Hero;
import com.mvc.dao.HeroDao;


public class HeroList extends HttpServlet {
	
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		request.setCharacterEncoding("UTF-8");
		response.setContentType("text/html; charset=UTF-8");
		//驗證是否登入成功 暫時先忽略
		if(null == request.getSession().getAttribute("user") || "" == request.getSession().getAttribute("user")) {
			response.sendRedirect("login.html");
			return;
		}
		
		//以下為分頁程式碼,暫時先忽略
		int total = new HeroDao().getTotal();
		int start = 0;
		int count = 5;
		int lastPage = 0;
		if(total/count == 0) {
			lastPage = total -count;
		}else {
			lastPage = (total/count) * count;
		}
		try {
			int next = Integer.parseInt(request.getParameter("next"));
			if(next <= total) {
				start = next;
			}else {
				start = (total/count) * count;
			}
		} catch (Exception e) {
			start = 0;
		}

        //從這裡開始看,新建集合物件
		List<Hero> heros = new ArrayList<Hero>();
		
        //忽略,此為分頁內容
		int xiaye = start + count;
		int shangye = xiaye - (count * 2);
		if(shangye < 0) {
			shangye = 0;
		}

        //將資料庫查詢結果賦值給heros,並存在session中,key為hero1,重定向到前端頁面。
		heros = new HeroDao().getHeroList(start,count);
		request.setAttribute("xiaye", xiaye);
		request.setAttribute("shangye", shangye);
		request.setAttribute("lastPage", lastPage);
		request.getSession().setAttribute("hero1", heros);
		request.setAttribute("hero", heros);
		request.getRequestDispatcher("heroList.jsp").forward(request, response);
	}

}

4. 此時伺服器地址將轉向:http:localhost:8080/mvc/heroList.jsp,並在session中存了一個key為hero1的資料。接下來是我們的View層heroList.jsp如何將session中的值取出,然後顯示出來的時候了。

在改JSP中,我們需要用到jstl的知識和EL表示式(兩者我的JSP部落格中有介紹,很簡單),jar包已經匯入好。

加入指令<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>告訴瀏覽器我們使用c標籤。

先上程式碼:

<%@page import="com.mvc.bean.Hero"%>
<%@page import="java.util.List"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" isELIgnored="false"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<style>
	a{
		text-decoration: none
	}
</style>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>英雄列表</title>
</head>
<body>

<h2>***************使用JSP獲取的資料庫中的集合值***************</h2>
<%
	List<Hero> hlist = (List<Hero>)request.getSession().getAttribute("hero1");
%>
<table style="width: 400px;" align="center" border="1">
	<tr>
		<td>id</td>
		<td>name</td>
		<td>hp</td>
		<td>damage</td>
	</tr>
	<%for(int i = 0; i < hlist.size(); i++) { %>
	<tr>
		<td><%=hlist.get(i).getId() %></td>
		<td><%=hlist.get(i).getName() %></td>
		<td><%=hlist.get(i).getHp() %></td>
		<td><%=hlist.get(i).getDamage() %></td>
	</tr>
	<%} %>
</table>
<h2>***************使用JSTL中的forEach獲取的資料庫中的集合值***************</h2>
<table style="400px;"border="1" bordercolor="lightSkyBlue" align="center" cellspacing="0">
	<tr>
		<td>id</td>
		<td>name</td>
		<td>hp</td>
		<td>damage</td>
		<td colspan="2" align="center"><font color="green">操作</font></td>
	</tr>
<c:forEach items="${hero1}" var="hero" varStatus="st">
	<tr>
		<td>${hero1.id}</td>
		<td>${hero1.name}</td>
		<td>${hero1.hp}</td>
		<td>${hero1.damage}</td>
		<td><a href="update?id=${hero1.id}"><font color="green">更新</font></a></td>
		<td><a href="del?id=${hero1.id}"><font color="red">刪除</font></a></td>
	</tr>
</c:forEach>
</table>
<div align="center">
<a href="?next=0">首頁</a>
<a href="?next=${shangye}">【上一頁】</a>
<a href="?next=${xiaye}">【下一頁】</a>
<a href="?next=${lastPage}">末頁</a>
</div>
<hr>
<p>當前線上使用者數為:${online_number}</p>
<a href="goOut">退出登入</a>
</body>
</html>

我這裡使用了兩種方式展示資料,一種是純JSP展示,一種是比較方便的EL表示式展示資料。分頁和刪除更新和當前使用者線上數那裡先不用看。只看表格中的資料展示即可。

對程式碼進行解釋:伺服器帶著session資料進入到該頁面,我們獲取session中資料的方式有很多種,這裡使用了JSP的隱式物件request.getSession.getAttribute(heros1)方法來獲取或者使用EL表示式中${heros1}。因為我們heros1是個集合,集合中存的是hero物件,我們需要的資料在物件中,也就是heros1[*].hero.name這樣的形式獲取,所以我們需要遍歷資料,完成取數。這裡使用的是JSTL中的forEach。通過這種方式,我們前端頁面成功的顯示出了資料庫中的內容。因為使用了分頁,只顯示五條資料

5. 配置web.xml,配置我們的servlet,位址列輸入:http:localhost:8080/mvc/heroList,請求伺服器,就會到我們顯示英雄的頁面啦。

  
	<servlet>
		<servlet-name>HeroList</servlet-name>
		<servlet-class>HeroList</servlet-class>	
	</servlet>
	<servlet-mapping>
		<servlet-name>HeroList</servlet-name>
		<url-pattern>/heroList</url-pattern>
	</servlet-mapping>

 

6. 其實你如果會操作的這些,對於MVC你已經掌握了,他就是servlet和JSP的結合版,沒有什麼特殊的呀

三:刪改操作。增加操作這裡不再寫了,有興趣的朋友可結合我的servlet部落格自行聯絡 。

1. 在heroDao中增加刪除和修改的方法,其中刪除很簡單,只需要獲取該條資料的ID對應去資料庫執行刪除操作即可,更新則多了一部,需要先從資料庫中獲取到該條資料,然後在編輯後儲存。具體程式碼如下:

//刪除
public void delHero(int id) {
		try {
			String sql = "delete from hero where id = ?";
			Connection con = getConnection();
			PreparedStatement pm = con.prepareStatement(sql);
			pm.setInt(1, id);
			pm.execute();
			pm.close();
			con.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
//獲取
	public Hero getById(int id) {
		Hero hero = new Hero();
		try {
			String sql = "select * from hero where id=?";
			Connection con = getConnection();
			PreparedStatement pm = con.prepareStatement(sql);
			pm.setInt(1, id);
			ResultSet rs = pm.executeQuery();
			while(rs.next()) {
				hero.setId(rs.getInt("id"));
				hero.setName(rs.getString("name"));
				hero.setHp(rs.getFloat("hp"));
				hero.setDamage(rs.getInt("damage"));
			}
			pm.close();
			con.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return hero;
	}

//更新	
	public void updateHero(Hero hero) {
		try {
			Connection con = getConnection();
			String sql = "update hero set name=?,hp=?,damage=? where id = ?";
			PreparedStatement pm = con.prepareStatement(sql);
			pm.setString(1, hero.getName());
			pm.setFloat(2, hero.getHp());
			pm.setInt(3, hero.getDamage());
			pm.setInt(4, hero.getId());
			pm.execute();
			pm.close();
			con.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

2. 配置web,xml檔案,配置刪除和更新的servlet:

3.. 編寫delHero.java和updateHero.java類,首先一定前提:觀察上面的heroList.jsp中,對應的刪除和更新連結會傳遞一個引數,就是該條資料的id值。我們在頁面中點刪除時,連線訪問servlet,servlet訪問delHero類,執行service方法。我們在delHero.java中通過request.getParameter("id")獲取到該條資料的ID值,然後執行刪除方法,最後重定向到heroList這個servlet中去,再去查詢一遍資料庫,顯示heroList.jsp,程式碼如下:

public class DelHero extends HttpServlet {
	protected void service(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
		int id = Integer.parseInt(request.getParameter("id"));
		new HeroDao().delHero(id);
		response.sendRedirect("heroList");
	}
}

4. 點更新後,根據servlet配置會跳轉到獲取資料的類中,獲取傳過來的id值,執行getById方法,獲取資料,將資料儲存在request中,伺服器跳轉到editHero.jsp,程式碼如下:

public class UpdateHero extends HttpServlet {

	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException,IOException {
		Hero hero = new Hero();
		hero = new HeroDao().getById(Integer.parseInt(request.getParameter("id")));
		request.setAttribute("hero", hero);
		request.getRequestDispatcher("editHero.jsp").forward(request, response);
	}
}

在編輯頁面editHero.jsp,該頁面中會獲取到這條需要更新的資料的所有值,顯示出來,把需要修改的資料進行修改後,點選更新按鈕,重新提交請求到實現更新的servlet進行執行update操作。

<title>修改英雄屬性</title>
</head>
<body>
<form action="update1Hero" method="get">
	英雄:<input type="text" name="name" value="${hero.name}">
	血量:<input type="text" name="hp" value="${hero.hp}">
	傷害:<input type="text" name="damage" value="${hero.damage}">
	<input type="hidden" name="id" value="${hero.id}">
	<input type="submit" value="更新">
</form>
</body>
</html>

此時點更新後會請求地址update1Hero,web.xml中配置了該servlet的對應的類,然後在該類中獲取到所有通過form表單提交的引數值,將引數值存放在hero物件中,然後執行updateHero()方法。完成更新。最後重定向到listHero中,重新查詢資料庫中所有的資料,顯示到heroList.jsp裡面。更新類的程式碼如下:

public class Update1Hero extends HttpServlet {

	protected void service(HttpServletRequest request, HttpServletResponse response)
	throws ServletException, IOException{
		Hero hero = new Hero();
		hero.setId(Integer.parseInt(request.getParameter("id")));
		hero.setName(request.getParameter("name"));
		hero.setDamage(Integer.parseInt(request.getParameter("damage")));
		hero.setHp((float)Double.parseDouble(request.getParameter("hp")));
		new HeroDao().updateHero(hero);
		response.sendRedirect("heroList");
		//request.getRequestDispatcher("heroList").forward(request, response);
	}
}

四:分頁:

分頁的核心知識點其實就在於sql的limit a,b的用法,他的意思就是從第a條資料開始查詢,查詢b跳,a從基數0開始數。

核心sql:select * from hero order by id asc limit ?,?   其中order by id asc是按照id降序排序的意思。也就是說,我們只需要修改後面的兩個引數,即可完成分頁功能。我麼分別在頁面中給上一頁,下一頁,首頁,未頁賦予一個引數,並將該引數通過連線改變位址列的地址,將地址帶上引數訪問伺服器。然後在servlet中對這些引數完成邏輯處理,即可輕鬆實現分頁功能。其中分頁查詢的方法如下:有兩個引數

public List<Hero> getHeroList(int start, int count) {
		List<Hero> listHero = new ArrayList<Hero>();
		try {
			Connection con = getConnection();
			String sql = "select * from hero order by id asc limit ?,?";
			PreparedStatement pm = con.prepareStatement(sql);
			pm.setInt(1, start);
			pm.setInt(2, count);
			ResultSet rs = pm.executeQuery();
			while(rs.next()) {
				Hero hero = new Hero();
				int id = rs.getInt("id");
				String name = rs.getString("name");
				int damage = rs.getInt("damage");
				float hp = rs.getFloat("hp");
				hero.setId(id);
				hero.setName(name);
				hero.setHp(hp);
				hero.setDamage(damage);
				listHero.add(hero);
			}
			pm.close();
			con.close();
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		return listHero;
	}

servlet中處理引數的邏輯如下:

int total = new HeroDao().getTotal();
		int start = 0;
		int count = 5;
		int lastPage = 0;
		if(total/count == 0) {
			lastPage = total -count;
		}else {
			lastPage = (total/count) * count;
		}
		try {
			int next = Integer.parseInt(request.getParameter("next"));
			if(next <= total) {
				start = next;
			}else {
				start = (total/count) * count;
			}
		} catch (Exception e) {
			start = 0;
		}
		List<Hero> heros = new ArrayList<Hero>();
		
		int xiaye = start + count;
		int shangye = xiaye - (count * 2);
		if(shangye < 0) {
			shangye = 0;
		}
		heros = new HeroDao().getHeroList(start,count);
		request.setAttribute("xiaye", xiaye);
		request.setAttribute("shangye", shangye);
		request.setAttribute("lastPage", lastPage);
		request.getSession().setAttribute("hero1", heros);
		request.setAttribute("hero", heros);
		request.getRequestDispatcher("heroList.jsp").forward(request, response);

 大體對處理分頁的邏輯進行解析:

total就是獲取資料庫中的總記錄條數。用以判斷未頁不在跳轉。

第一次訪問servlet時,根據邏輯我們判斷好了lastPage的值和xiaye的值和shangye的值,將該值傳遞給前端頁面,點選時通過next引數傳遞過來。

假如一共有13頁,在前臺中點選下一頁,位址列傳遞一個next的引數,該值初始值為5(因為第一次訪問servlet時,這個值就被賦予了5),next=5在servlet中獲取之後,判斷下一頁的引數是否超過了總記錄條數,如果超過則start的值就是總記錄條數除以分頁條數取整然後乘以每頁條數,也就是13/5 * 5=10,也就是最後一頁的開始數。如果沒有超過,則讓start的值等於這個next,此時start=5,然後再設定準備傳遞給前臺頁面中下一個下一個引數的值,也就是xiaye:5+5=10,然後傳遞給前臺下次點選上頁時應該傳遞的值,shangye:10-(5*2)=0,呼叫方法getHeroList(start,count),其中count值是不變的,變的是start的值,此時該值為5,則資料庫去查詢從第六條資料開始,查詢五條,也就是我們的第二頁。

此時跳轉到第二頁,我們再點選下一頁時,next是幾呢?跳轉之前就以為為我們算好了,此時next是10,沒有超過總記錄條數,start=10,下一個下頁的值更新為:15,下一個上頁的值更新為5,然後執行getHeroList(10,5)顯示第三頁。

第三頁中再點選下一頁,next=15了,超過了記錄條數,start=10,在設定下一個下頁的值為15,然後去查詢getHeroList(10,5)仍然顯示第三頁,在去點選下一頁的時候,start還是等於10,仍然會查詢getHeroList(10,5)第三頁。

如果在第三頁點選上一頁的時候,此時next的值是什麼呢?我們跳轉之前就設定好了,此時next傳遞的是5,沒有超過記錄條數,start=5,設定下一個下頁的值為10,下一個上頁的值為0,然後執行getHeroList(5,5)返回到第二頁中。

如果在第一頁點選上一頁呢?next=0,start=0,shangye的值:5-5*2=-5,增加了判斷if(shangye < 0),shangye就=0,則此時執行getHeroList(0,5)還是查詢第一頁的值。

稍微思考一下就能理解這個邏輯了,演算法好的人一看應該就懂。我竟然贅述了這麼多...

五:至此,MVC設計模式中小demo的增刪該查已經全部完成了,就這麼簡單。

在這個demo中我增加了登入功能和監聽器監聽session和request的變動,如果需要了解,我會在下面的部落格中進行解析和分享。也就是大家之前看到的驗證登入(判斷使用者是否登入,如果登入可以訪問全部頁面,如果未登入,則只能訪問登入頁面,無法訪問英雄列表等頁面)和監聽線上人數