基于Servlet的学生管理系统
一、緣起
四個月前,我曾經寫過一個基于JSP的學生管理系統《基于JSP的學生管理系統》,由于要交作業的關系 ,這一次我又帶來了一個基于Servlet的學生管理系統,在原有的基礎上新增了其他功能,后端使用了經典MVC分層架構,前端使用了一些比較新穎的框架,介系你沒有van過的船新版本 。
二、使用到的技術
1.前端
- HTML5
- CSS3
- JQuery 3.2.1
- 組件庫:BootStrap 4.1.0
- 表單校驗:nice-validator 1.1.4
- Web應用框架:VUE
- 異步請求:AJAX
2.后端
- Servlet
3.使用到的工具包
- 連接數據庫:mysql-connector-java 5.1.6
- 單元測試:junit 4.12
- MD5加密:commons-codec 1.10
- bean工具:commons-beanutils 1.9.3
- Google的JSON生成器:gson 2.3.1
4.數據庫
- MySQL 5.7.10
4.本人電腦環境
- 操作系統:Windows10
- Java版本:1.8.0_172
- TomCat:8.5
- IDE:MyEclipse 2017 CI
- 瀏覽器:Google Chrome 69 和Firefox
三、項目相關
1.項目下載
項目已在GitHub上托管開源,請遵守MIT協議進行使用。我的GitHub
2.項目安裝
詳情請查看我項目GitHub下的ReadMe
3.在線體驗
本項目已部署在我的服務器上,以供各位同學進行體驗,若在體驗過程發現有BUG,請務必在GitHub發出Issuse或者在下面評論留言,感謝您的反饋。在線體驗
由于超級管理員能夠刪除其他管理員賬號,故這里只能給出普通管理員賬號供大家使用,超級管理員和普通管理員的差別是普通管理員少了一個管理員管理模塊。
- 賬號:李白
- 密碼:test1234
請各位同學務必保持數據庫整潔,在下線前務必刪除體驗新增的內容,以給接下來體驗的人有個良好體驗。
也由于我的服務器只是一個性能一般的學生服務器,沒有啥防護能力,求各位黑客大大放小弟服務器一條生路,如果服務器出現異常,在線體驗功能只能夠下線了。
四、管理頁面預覽
1.登錄頁面
2.主頁面
3.學生添加功能
4.專業管理
5.學院管理
五、項目實現功能亮點
六、項目結構
1.前端
(1)登錄頁面布局
(2)主頁面
(3)內容管理
2.后端
(1)后端接口
3.數據庫E-R圖
七、關鍵功能實現
1.如何實現Servlet返回JSON格式的數據?
一般來說,Servlet并不像Spring那樣有返回JSON數據的能力,所以我們只能依賴第三方的工具類,這里我推薦使用Google公司開發的GSON工具。它的用法很簡單,只需要使用gson.toJson()方法即可,這個方法能夠轉換例如String字符串,List列表,Map鍵值對等,一般我們都會使用Map<String,Object>來進行存儲,如以下步驟:
我們在MajorManagementServlet.java這個servlet中
當中有這樣一段語句
response.setHeader("Content-Type", "application/json");//設置響應頭的內容類別為JSON,以便前端更好識別 response.setCharacterEncoding("UTF-8");//設置相應格式為UTF-8,防止輸出中文亂碼 PrintWriter out = response.getWriter();//獲取response.getWriter(),方便最后把JSON內容輸出完成以上內容后,我們在使用了一個Map來填裝了數據,然后通過String res = gson.toJson(map);來轉換成JSON格式,最后使用out.print(res);
Gson gson = new Gson();Map<String,Object> map = new HashMap<String, Object>();map.put("majorList", majorList);map.put("allMajorCount", listCount);map.put("prePage", prePage);map.put("nextPage", nextPage);map.put("pageNum", pageNum);map.put("page", page);String res = gson.toJson(map);out.print(res);最終獲取的效果以JSON格式輸出
2.如何連接數據庫?
總所周知,使用JDBC查詢數據庫,有一個很經典的模板如下
try {Class.forName(driverClass);connection = DriverManager.getConnection(url, user, password);statement = connection.createStatement();rs = statement.executeQuery(sql);rowSet = new CachedRowSetImpl();rowSet.populate(rs);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException e) {e.printStackTrace();}finally {try {rs.close();statement.close();connection.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}可是我們需要頻繁地查詢數據庫,如果每一次這樣寫不就會很累?俗話說得好:“不要重復造輪子”,輪子只需要造一次就夠了,所以我們應該把它抽象出來,單獨成為一個工具類MySQLConnectionUtils。
package com.management.utils;import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties;import javax.sql.RowSet; import javax.sql.rowset.CachedRowSet;import com.sun.rowset.CachedRowSetImpl;/*** 數據庫連接工具* @author CheungChingYin**/ public class MySQLConnectionUtils {// 用于連接數據庫的工具類public static CachedRowSetImpl mySQLResult(String sql) {String path = MySQLConnectionUtils.class.getClassLoader().getResource("db.properties").getPath();FileInputStream in = null;Properties properties = new Properties();try {in = new FileInputStream(path);} catch (FileNotFoundException e1) {e1.printStackTrace();}try {properties.load(in);} catch (IOException e1) {e1.printStackTrace();}String user = properties.getProperty("jdbc.user");String password = properties.getProperty("jdbc.password");String driverClass = properties.getProperty("jdbc.driverClass");String url = properties.getProperty("jdbc.jdbcUrl");Connection connection = null;Statement statement = null;ResultSet rs = null;CachedRowSetImpl rowSet = null;try {Class.forName(driverClass);connection = DriverManager.getConnection(url, user, password);statement = connection.createStatement();rs = statement.executeQuery(sql);rowSet = new CachedRowSetImpl();rowSet.populate(rs);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException e) {e.printStackTrace();}finally {try {rs.close();statement.close();connection.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}return rowSet;}public static Connection mySQLConnection() {String path = MySQLConnectionUtils.class.getClassLoader().getResource("db.properties").getPath();FileInputStream in = null;Properties properties = new Properties();try {in = new FileInputStream(path);} catch (FileNotFoundException e1) {e1.printStackTrace();}try {properties.load(in);} catch (IOException e1) {e1.printStackTrace();}String user = properties.getProperty("jdbc.user");String password = properties.getProperty("jdbc.password");String driverClass = properties.getProperty("jdbc.driverClass");String url = properties.getProperty("jdbc.jdbcUrl");Connection connection = null;try {Class.forName(driverClass);connection = DriverManager.getConnection(url, user, password);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException e) {e.printStackTrace();}return connection;} }db.properties相關內容
jdbc.user=root jdbc.password=123456 jdbc.driverClass=com.mysql.jdbc.Driver jdbc.jdbcUrl=jdbc\:mysql\://localhost\:3306/student?useUnicode\=true&characterEncoding\=utf-8 jdbc.initPoolSize=5 jdbc.maxPoolSize=10由于我們會很頻繁地調用這個類,所以我們就需要考慮讓它成為一個靜態方法,以便于我們每次調用的時候不需要創建一個對象然后再使用。使用properties文件把數據庫的一些信息,如地址、數據庫賬戶、數據庫密碼存儲,以便日后項目遷移需要修改數據庫信息。
在一開始,我的public static CachedRowSetImpl mySQLResult(String sql)方法的返回值類型是使用ResultSet的,如這樣public static ResultSet mySQLResult(String sql),然后使用了一端時間后才發現,當發出一大堆數據庫連接的時候,由于我沒有關閉釋放連接,就是在finally沒有connection.close();,所以會造成數據庫連接過多導致被數據庫拒絕連接,出現一個叫做Can not connect to MySQL server Error: Too many connections的異常錯誤,發現這個問題后我立馬在方法的finally塊中加入了
然后返回一個ResultSet結果集,雖然連接問題是解決了,可是新問題又出現了,控制臺發出了一個異常java.sql.SQLException: Operation not allowed after ResultSet closed(結果集已關閉,無法操作。),如果我在返回結果集之前關閉了結果集,那就不能夠操作結果集了;如果我不關閉結果集,數據庫連接一直都會存在,然后重復操作幾次又會出現數據庫連接數過多,拒絕訪問的異常了,那改怎么辦?
最終我在這篇博文上找到了答案《java開發中如何在ResultSet結果集關閉后,還能使用數據庫數據。》,只需要返回類型使用CachedRowSetImpl即可,詳細請查閱這篇文章。
3.如何讓用戶輸入的密碼進行加密放到數據庫中?
說到數據加密,我們不得不提到Apache公司的開發的一個常用加密工具類commons-codec。
這個項目的密碼是使用MD5進行加密的(也不能說是加密,因為它不可解密,只能說是一種認證,防止后臺的的數據庫操作員能夠輕易地獲得密碼)
在加入了commons-codec的JAR包后,我把加密方法做成了一個抽象方法,以便以后使用
PasswordEncryptionUtils類
像是管理員賬號李白,密碼是test1234,可是到了數據庫后密碼變成了這樣
一串沒有意義的亂碼。
所以在注冊和登錄的時候,一定要先把密碼加密,不然最后會應為對不上數據庫所存放的密碼導致密碼不正確。
4.分頁功能
這一次的分頁功能我是在后臺完成的,為此我專門做了一個分頁的工具PageUtils,其中的邏輯和我上一次的項目分頁邏輯大致一致。
package com.management.utils;import java.util.LinkedList; import java.util.List;/*** 分頁工具* @author CheungChingYin**/ public class PageUtils {/*** 上一頁邏輯* @param page* @return*/public static Integer prePageHandler(Integer page) {Integer prePage;if (page - 1 == 0) {//如果當前頁-1為0,則表示當前頁為第一頁,prePage = 1;//前一頁只能為第一頁} else {prePage = page - 1;}return prePage;}/*** 下一頁邏輯* @param page* @param listCount* @return*/public static Integer nextPageHandler(Integer page, Integer listCount) {Integer PAGESIZE = ConstantUtils.Page.PAGESIZE;Integer pages = (listCount % PAGESIZE == 0) ? (listCount / PAGESIZE) : (listCount / PAGESIZE + 1);Integer nextPage;if (page == pages) {//如果當前頁等于最大頁數nextPage = pages;//下一頁只能等于最大頁數} else {nextPage = page + 1;}return nextPage;}/*** 列出頁碼列表* @param page* @param listCount* @return*/public static List<Integer> pageHandler(Integer page,Integer listCount) {List<Integer> list = new LinkedList<Integer>();Integer PAGESIZE = ConstantUtils.Page.PAGESIZE;Integer PAGENUM = ConstantUtils.Page.PAGENUM;Integer pages = (listCount % PAGESIZE == 0) ? (listCount / PAGESIZE) : (listCount / PAGESIZE + 1);Integer minPages = (page - PAGENUM > 0) ? (page - PAGENUM) : (1);//和上一頁同理Integer maxPages = (page + PAGENUM >= pages)?(pages):(page + PAGENUM);//與下一頁同理for(int i = minPages;i<= maxPages;i++){list.add(i);//添加最小頁到最大頁之間的頁碼}return list;}/*** 總共頁數邏輯* @param listCount* @return*/public static Integer pagesHandler(Integer listCount){Integer PAGESIZE = ConstantUtils.Page.PAGESIZE;Integer pages = (listCount % PAGESIZE == 0) ? (listCount / PAGESIZE) : (listCount / PAGESIZE + 1);return pages;}}在每次請求需要分頁的時候,首先傳入一個page參數代表當前頁數,還有一個就是從數據庫獲得內容的總條數,從而計算出需要分多少頁,類中的PAGESIZE和PAGENUM其實是一個固定的常量,為了解耦才把常量抽取出來。
package com.management.utils;/*** 相關常量* @author CheungChingYin**/ public interface ConstantUtils {public static class Page {public static final Integer PAGESIZE = 10;//一頁顯示多少條資料public static final Integer PAGENUM = 3;//頁碼展示數量} }在每次需要用到分頁功能的時候,都會有進行分頁判斷,以SearchStudent的片段代碼為例
Integer page = Integer.parseUnsignedInt(request.getParameter("page"));//獲取傳來的參數當前頁Integer listCount = null;if (search == null || page==null) {return;}if (search.matches("\\d+")) {studentList = new LinkedList<Student>();Student s = service.searchStudentById(search);studentList.add(s);listCount = studentList.size();} else {studentList = service.searchStudentByName(search);listCount = studentList.size();Integer pages = PageUtils.pagesHandler(listCount);if(page == pages){//由于搜索結果如果有多個時候,是需要分頁的,若當前頁等于最大頁studentList = service.searchStudentByName(search).subList((page - 1) * 10, listCount);//列表只展示到最后一條,不然會越界}else{studentList = service.searchStudentByName(search).subList((page - 1) * 10, page * 10);//列表只能存10條數據,達到偽分頁效果}}Integer prePage = PageUtils.prePageHandler(page);Integer nextPage = PageUtils.nextPageHandler(page, listCount);List<Integer> pageNum = PageUtils.pageHandler(page, listCount);map.put("studentList", studentList);map.put("allStudentCount", listCount);map.put("prePage", prePage);map.put("nextPage", nextPage);map.put("pageNum", pageNum);map.put("page", page);map.put("search",search);通過JSON把上一頁、下一頁、當前頁、顯示的頁數(即超鏈接中顯示的數字頁數)傳回前端,讓前端自己處理即可。
5.搜索功能如何判斷搜索的是ID還是姓名?
同樣以SearchStudent為例,通過前端傳來了一個搜索內容search,我們只需要通過字符串自帶的一個方法,String.matches()進行正則表達式判斷,代碼段中的"\\d+"指的是判斷字符串是不是一位以上純數字,如果返回判斷是正確,則表明是學號ID搜索,只需要向service層調用searchStudentById()方法即可,由于搜索內容除了學號ID之外就只有姓名了,所以else是能是調用service層的searchStudentByName()方法。
6.學生管理中的學院欄和專業欄如何實現二級聯動表單?
這個就涉及到了JavaScript了,我的想法是這樣的(以添加學生信息為例)
這樣看起來貌似步驟很多,但其實有一些步驟是連起來做的
以下代碼源自StudentManagement.js代碼片段
步驟1~3代碼
步驟4~7代碼
/** 學院按鈕改變時* 當選中學院下拉菜單中的一項時,通過Ajax加載相對應的學院專業*/ $("#college").change(function() {var collegeId = $(this).val();$("#major option:not(:first)").remove();$.get("getMajor?collegeId=" + collegeId, function(data, status) {var major = data;var res = "";for (var i = 0; i < major.length; i++) {res += "<option value='" + major[i].id + "'>" + major[i].name + "</option>";}$("#major").append(res);}); });7.如何實現主頁中部分地方刷新?
像這樣實現網頁中部分位置刷新,其實只要用到JQuery中的load()方法即可,以下代碼出自Ajax_Main.js
指定一個id為contain的div對傳來的地址進行刷新即可。
8.如何按不同權限管理員展示不同的頁面
超級管理員比普通管理員登錄時,側欄會多出一個管理員管理功能,普通權限的管理員是沒有的,效果如下
張三是超級管理員,李白則是普通管理員
要實現這個功能,首先需要在登錄成功的時候把獲得的權限存放在session域中,在主頁Ajax_Main.jsp中使用一個隱藏的<input>把值取出
代碼出自LoginServlet.java中
代碼出自Ajax_Main.jsp
只要頁面在初始化的時候檢測這個id為permission的輸入框中的value,只要值不等于1的話,就把管理管理員這個超鏈接移除,代碼出自Ajax_Main.js
僅僅靠前端是不保險的,有些了解我的代碼,直接在頁面上修改值不就能顯示管理員管理了嗎?所以說后端我們也需要驗證權限,雙份保障雙重保險。
在Servlet映射管理員管理相應JSP那個接口(即AdministratorManagementUIServlet)做手腳
先判斷session中有沒有admin,沒有即表示是跳過登錄頁面進行訪問的,所以直接重定向回登錄頁面。然后再判斷存在session域中的permission,只有權限(permission)為1的時候才能夠跳轉到管理員管理的頁面中。
9.如何實現cookie免登陸?
方法很簡單,只要在登錄的時候,檢查cookie和session是否都同時存在,如果,只要登錄成功,檢測多選框有沒有選擇(不要問我為啥不用單選框,因為多選框的展示效果比單選框好),如果選擇了則寫cookie,沒有選擇就按普通流程登錄
Created with Rapha?l 2.2.0登錄頁打開是否有對應cookie登錄成功輸入用戶名密碼用戶名和密碼是否正確提示用戶名或密碼錯誤yesnoyesno跳轉登錄頁面servletLoginUIServlet
package com.management.web.UI;import java.io.IOException;import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession;/*** 跳轉到登錄頁面* @author CheungChingYin**/ @WebServlet(description = "跳轉到登錄界面", urlPatterns = { "/Login" }) public class LoginUIServlet extends HttpServlet {private static final long serialVersionUID = 1L;protected void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {HttpSession session = request.getSession();Cookie[] cookies = request.getCookies();if(cookies != null){for (Cookie cookie : cookies) {if (cookie.getName().equals("JSESSIONID")) {// 判斷是否存在第一次登錄時存放的cookieif (session.getAttribute("admin") != null) {// 判斷服務器里面的存放的cookie是否存在// 直接重定向到主界面response.sendRedirect(request.getContextPath() + "/Home");return;}}}}// 如果檢測不到cookie就轉發到登錄界面request.getRequestDispatcher("/WEB-INF/jsp/Login.jsp").forward(request, response);}protected void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {doGet(request, response);}}登錄驗證servletLoginUIServlet
package com.management.web.controller.administrator;import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession;import com.management.entities.Administrator; import com.management.service.AdministratorService; import com.management.service.impl.AdministratorServiceImpl; import com.management.utils.WebUtils;/*** 管理員登錄功能* 需要傳入參數* request:* 登錄界面表單:* user(管理員姓名)* password(管理員密碼)* rememberMe(可選)(記住我選項)* * @author CheungChingYin**/ @WebServlet("/LoginServlet") public class LoginServlet extends HttpServlet {private static final long serialVersionUID = 1L;protected void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {request.setCharacterEncoding("UTF-8");Administrator admin = WebUtils.request2Bean(request, Administrator.class);//獲得表單傳來的參數AdministratorService service = new AdministratorServiceImpl();HttpSession session = request.getSession();String user = admin.getUser();String password = admin.getPassword();response.setHeader("Content-type","text/html;charset=UTF-8");boolean loginResult = service.login(user, password);//檢查用戶名、密碼能否通過登錄Integer permission = service.searchAdministratorByName(user).getPermission();if (loginResult) {//登錄通過執行事件if (request.getParameter("remeberMe") != null && request.getParameter("remeberMe").equals("on")) {//檢查是否勾選了記住我,需要先檢查獲取是否為空,不然會報空指針異常session.setAttribute("admin", user);session.setAttribute("permission",permission);session.setMaxInactiveInterval(7 * 24 * 3600);// Session保存7天Cookie cookie = new Cookie("JSESSIONID", session.getId());cookie.setMaxAge(7 * 24 * 3600);// cookie的有效期也為7天cookie.setPath("/");response.addCookie(cookie);//設置Cookieresponse.getWriter().write("<script language='JavaScript'>alert('登錄成功');window.location.href='"+request.getContextPath()+"/Home'</script>");} else {//沒有勾選“記住我”,使用非cookie功能登錄session.setAttribute("admin", user);session.setAttribute("permission",permission);response.getWriter().write("<script language='JavaScript'>alert('登錄成功');window.location.href='"+request.getContextPath()+"/Home'</script>");}} else {//登錄失敗response.getWriter().write("<script language='JavaScript'>alert('您的用戶名或密碼有誤,請重新輸入或者注冊');window.location.href='"+request.getContextPath()+"/Login'</script>");}}protected void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {doGet(request, response);}}10.存放JSP需要注意的問題
由于在servlet中,每一個jsp頁面都應該由相對應的servlet進行跳轉,而不是讓瀏覽器直接訪問到。所以我們必須把所有的JSP頁面都存放在WEB-INF目錄下,只有這樣才能不被外界直接訪問到JSP,而要通過相應的Servlet進行跳轉。而其他靜態資源,如css、JavaScript、images等的資源文件由于保護等級沒有那么重要,所以可以直接把這些靜態資源放在WebRoot目錄下,以方便調用。
11.前端表單校驗如何實現?
在前端的表單校驗中,我使用了第三方插件nice-validator,這個插件是國人開發的,有中文文檔,只要閱讀了快速上手后就能夠自己實現表單校驗了。
以專業添加表單為例,代碼出自MajorManagement.js
八、在開發的時候遇到的坑
在開發前端頁面的時候,有幾個按鈕設置了點擊事件,而這些按鈕是通過JavaScript語句動態創建的,導致這些按鈕的點擊事件失效了,無論怎樣按都不能觸發點擊事件。
這個好像是因為DOM渲染后,已經綁定好相應的點擊事件,但是由于有一些元素如按鈕,是通過JavaScript動態新建的,所以沒有被綁定事件,導致無法觸發點擊事件。(由于本人對前端一知半解,所以如果有發現這一段話有錯的同學請務必寫評論糾正)
解決方法:只需要使用JQuery的on()方法即可,詳細使用方法可以參考這篇博文《js/jq 動態添加的元素不能觸發綁定事件解決方案》
九、開發感想
????這個作業花了我24天時間才完成,當然在當中也少不了有摸魚 的時間,估計如果在沒有課,不摸魚的情況下大概12~16天就能夠完成。這一次的代碼估摸2000行應該是有的,開發難度還好,就是前端的問題比較多,因為自己不太熟悉前端框架,JQuery、Vue、BootStrap這些前端框架我都是在用到的時候才學,在之前并沒有系統性地學習過,用到啥功能就學啥,不求精通,只求能用。花了兩個小時看了下Vue的文檔,基本的功能就能夠使用了,所以說不要怕學習新的東西,你越怕就越不想去學,花點心思了解一下,總會有收獲的。
????這次開發總會想到各種奇奇怪怪的功能,有一些功能可能暫時實現不了,總想放棄,其實可以先放下這個問題,做點別的,有時候靈感一到,問題就會迎刃而解。還有,在開發之前一定要做一份計劃書,不要像我這樣一邊開發一邊想功能,有時候你寫到頂層如Web層,想實現一些功能,發現服務層沒有,服務層寫的時候又發現dao層沒有相應的方法,這樣一層層地從高層向下“補救”是不值得推薦的,就算寫得不詳細,不要緊,只要有大致框架即可。
????就快畢業了,希望自己能在畢業之前能夠做一些比較像樣的項目,以至于到外面面試也不會太丟人,繼續努力,“窮且益堅 不墜青云之志”!
總結
以上是生活随笔為你收集整理的基于Servlet的学生管理系统的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Ubuntu下SMPlayer播放器安装
- 下一篇: java信息管理系统总结_java实现科