Model
Model 層主要包含數據的類,這些數據一般是現實中的實體,所以,Model 層中類的定義常常和數據庫 DDL 中的 create 語句類似。
通常數據庫的表和類是一對一的關系,但是有的時候由於需求變化或者方便起見,Model 層的類有時不和數據庫中表相互對應。比如面向對象之組合屬性,在 Java 中可以用一個類組合另一個類,表示測試信息、對應多組測試用例的組合,(正常情況下,應該是一張表而不是兩張表),而數據庫是用兩張表存儲數據,利用外鍵關系表示測試信息、對應多組測試用例的關系。
由於數據繁多,為了簡化對象的映射,不使用JDBC,而采用持久化框架 MyBatis。
MyBatis 首先需要配置數據源:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- property from external resources --> <properties resource="config/mybatis/applications.properties"/> <!-- settings --> <settings> <setting name="cacheEnabled" value="true"/> <setting name="logImpl" value="STDOUT_LOGGING"/> <!-- <setting name="lazyLoadingEnabled" value="true" /> <setting name="multipleResultSetsEnabled" value="true" /> <setting name="useColumnLabel" value="true" /> <setting name="useGeneratedKeys" value="false" /> <setting name="autoMappingBehavior" value="PARTIAL" /> <setting name="defaultExecutorType" value="SIMPLE" /> <setting name="defaultStatementTimeout" value="25000" /> <setting name="safeRowBoundsEnabled" value="false" /> <setting name="mapUnderscoreToCamelCase" value="false" /> <setting name="localCacheScope" value="SESSION" /> <setting name="jdbcTypeForNull" value="OTHER" /> <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode ,toString" /> --> </settings> <!-- type aliases(full class name -> simple class name) --> <typeAliases> <!-- <typeAlias alias="Student" type="com.mybatis3.domain.Student" /> --> <package name="per.piers.onlineJudge.model"/> </typeAliases> <!-- type handlers --> <typeHandlers> <typeHandler handler="per.piers.onlineJudge.handler.SexTypeHandler" javaType="per.piers.onlineJudge.model.Sex" jdbcType="BOOLEAN"/> </typeHandlers> <!-- environment --> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> <environment id="production"> <transactionManager type="MANAGED"/> <dataSource type="JNDI"> <property name="data_source" value="java:comp/jdbc/mybatis"/> </dataSource> </environment> </environments> <!-- mappers location --> <mappers> <!-- <mapper url="file:///D:/mybatisdemo/app/mappers/TutorMapper.xml" /> <mapper class="com.mybatis3.mappers.TutorMapper" /> --> <mapper resource="mapper/UserMapper.xml"/> <mapper resource="mapper/QuestionMapper.xml"/> <mapper resource="mapper/CategoryMapper.xml"/> <mapper resource="mapper/TestDataMapper.xml"/> <mapper resource="mapper/TestInfoMapper.xml"/> <mapper resource="mapper/ScoreMapper.xml"/> <mapper resource="mapper/AdvisorMapper.xml"/> </mappers> </configuration>
之后創建工廠對象,再用它創建數據訪問對象(DataAccessObject,DAO):
@Bean public SqlSessionFactory sqlSessionFactory() throws IOException { ClassLoader classLoader = RootConfig.class.getClassLoader(); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(classLoader.getResourceAsStream("config/mybatis/mybatis-config.xml")); return sqlSessionFactory; } @Autowired @Bean public DataAccessObject dataAccessObject(SqlSessionFactory sqlSessionFactory) { return new DataAccessObject(sqlSessionFactory); }
DAO 對象負責數據訪問。首先以 Mapper 接口的方式定義訪問數據庫的函數,之后在 XML 文件中實現該函數,並提供具體實現(SQL 語句細節)。這樣做的好處一方面是防止命名錯誤,傳統 MyBatis 方式是根據函數名執行相關 SQL 語句的,不用接口書寫很容易出錯;另一方面有助於設計(接口)和實現分離,降低耦合性。
public interface UserMapper { public int insertUser(@Param("user")User user); public int updateUser(@Param("user")User user); public int deleteUser(@Param("user") User user); public User selectUser(@Param("user") User user); }
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="per.piers.onlineJudge.mapper.UserMapper"> <insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="user.id"> INSERT INTO users (email, password, name, sex, role, enabled) VALUES (#{user.email}, #{user.password}, #{user.name}, #{user.sex}, #{user.role}, #{user.enabled}) </insert> <update id="updateUser" parameterType="User"> UPDATE users <trim prefix="SET" suffixOverrides=","> <if test="user.email != null">email = #{user.email},</if> <if test="user.password != null">password = #{user.password},</if> <if test="user.name != null">name = #{user.name},</if> <if test="user.sex != null">sex = #{user.sex},</if> <if test="user.role != null">role = #{user.role},</if> <if test="user.enabled != null">enabled = #{user.enabled},</if> </trim> WHERE id = #{user.id} </update> <delete id="deleteUser" parameterType="User"> DELETE FROM users WHERE id = #{user.id} </delete> <select id="selectUser" parameterType="User" resultMap="userResult"> SELECT * FROM users <if test="user != null"> <where> <if test="user.email != null">email = #{user.email}</if> </where> </if> </select> <resultMap id="userResult" type="User"> <id column="id" property="id"/> <result column="email" property="email"/> <result column="password" property="password"/> <result column="name" property="name"/> <result column="sex" property="sex"/> <result column="enabled" property="enabled"/> <result column="role" property="role"/> </resultMap> </mapper>
MyBatis的可以處理基本類型,但有些類型需要自定義轉換,就需要 MyBatis 提供的 BaseTypeHandler 進行轉換。首先編寫 BaseTypeHandler(見下),之后在 MyBatis 配置文件中注冊(見上)。
package per.piers.onlineJudge.handler; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import per.piers.onlineJudge.model.Sex; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class SexTypeHandler extends BaseTypeHandler<Sex> { @Override public void setNonNullParameter(PreparedStatement preparedStatement, int i, Sex sex, JdbcType jdbcType) throws SQLException { preparedStatement.setInt(i, sex.getId()); } @Override public Sex getNullableResult(ResultSet resultSet, String s) throws SQLException { int sex = resultSet.getInt(s); if (resultSet.wasNull()) { return null; } else { return Sex.getSexType(sex); } } @Override public Sex getNullableResult(ResultSet resultSet, int i) throws SQLException { int sex = resultSet.getInt(i); if (resultSet.wasNull()) { return null; } else { return Sex.getSexType(i); } } @Override public Sex getNullableResult(CallableStatement callableStatement, int i) throws SQLException { int sex = callableStatement.getInt(i); if (callableStatement.wasNull()) { return null; } else { return Sex.getSexType(i); } } }
View
view 層主要是界面(頁面)。這里主要是 JSP 頁面,因為需要動態展示一些內容。其中還運用了 JavaScript 技術和 AJAX 技術,JavaScript 主要用作頁面輸入域校驗,AJAX 主要用於異步提交需要更新的內容。
<%@page contentType="text/html; charset=UTF-8" %> <%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@taglib uri="http://www.springframework.org/tags/form" prefix="sf" %> <%@taglib uri="http://www.springframework.org/security/tags" prefix="security" %> <!DOCTYPE html> <html> <head> <%@include file="../common/header.jspf" %> <title>注冊</title> </head> <body> <%@include file="../common/navbar.jspf" %> <div class="container"> <div class="page-header"> <h1>注冊</h1> </div> <div class="form-signin" oninput="satisfySubmit()"> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <div class="row"> <div class="col-md-8"> <div class="input-group"> <span class="input-group-addon" id="basic-addon1">郵箱*</span> <input type="email" id="email" name="email" value="${email}" class="form-control" placeholder="郵箱長度不超過40個字符" aria-describedby="basic-addon1" maxlength="40" required oninput="showEmailInputSuggestion()" disabled> </div> </div> <div class="col-md-4"> <p id="emailError" class="text-danger"></p> </div> </div> <br/> <div class="row"> <div class="col-md-8"> <div class="input-group"> <span class="input-group-addon" id="basic-addon2">密碼*</span> <input type="password" id="password" name="password" class="form-control" placeholder="密碼長度不少於6個字符,不多於20個字符,只能包括數字和字母" aria-describedby="basic-addon1" minlength="6" maxlength="20" required oninput="showAllPasswordSuggestion()"> </div> </div> <div class="col-md-4"> <p id="passwordError" class="text-danger"></p> </div> </div> <br/> <div class="row"> <div class="col-md-8"> <div class="input-group"> <span class="input-group-addon" id="basic-addon3">確認密碼*</span> <input type="password" id="repassword" name="repassword" class="form-control" placeholder="再次輸入密碼" aria-describedby="basic-addon1" minlength="6" maxlength="20" pattern="[\d\w]+" required oninput="showAllPasswordSuggestion()"> </div> </div> <div class="col-md-4"> <p id="repasswordError" class="text-danger"></p> </div> </div> <br/> <div class="row"> <div class="col-md-8"> <div class="input-group"> <span class="input-group-addon" id="basic-addon4">姓名</span> <input type="text" id="name" name="name" class="form-control" placeholder="你的姓名,可以很酷,不過最多只能有20個字符(中英皆可)" aria-describedby="basic-addon1" maxlength="20"> </div> </div> <div class="col-md-4"> <p></p> </div> </div> <br/> <div class="row"> <div class="col-md-8"> <div class="input-group"> <span class="input-group-addon" id="basic-addon5">性別</span> <div class="form-control"> <div class="radio-inline"> <label> <input type="radio" name="sex" id="optionsRadios1" value="MALE"> 男 </label> </div> <div class="radio-inline"> <label> <input type="radio" name="sex" id="optionsRadios2" value="FEMALE"> 女 </label> </div> </div> </div> </div> <div class="col-md-4"> <p></p> </div> </div> <br/> <input type="hidden" id="enabled" name="enabled" value="true"/> <input type="hidden" id="role" name="role" value="user"/> <input id="submit" type="button" value="不能注冊,請檢查相關項填寫是否正確" class="btn btn-danger" disabled onclick="registerUser()"> <br> <p id="success"></p> </div> </div> <%@include file="../common/footer.jspf" %> <script src="${pageContext.request.contextPath}/js/user/user.js"></script> <script src="${pageContext.request.contextPath}/js/user/register.js"></script> </body> </html>
function satisfySubmit() { var submit = document.getElementById("submit"); if (isEmailValid() && isPasswordValid() && isRepasswordValid()) { submit.setAttribute("type", "submit"); submit.setAttribute("value", "提交注冊"); submit.setAttribute("class", "btn btn-success"); submit.removeAttribute("disabled") } else { submit.setAttribute("type", "button"); submit.setAttribute("value", "不能注冊,請檢查相關項填寫是否正確"); submit.setAttribute("class", "btn btn-danger"); submit.setAttribute("disabled", ""); } }
function registerUser() { xmlhttp = new XMLHttpRequest(); if (xmlhttp != null) { var email = document.getElementById("email").value; var password = document.getElementById("password").value; var name = document.getElementById("name").value; var sexes = document.getElementsByName("sex"); var sex; for (var i = 0; i < sexes.length; i++) { if (sexes[i].checked) sex = sexes[i].value.toUpperCase(); } var enabled = document.getElementById("enabled").value; var role = document.getElementById("role").value; var csrf = document.getElementsByName("_csrf")[0].value; xmlhttp.onreadystatechange = stateChange; xmlhttp.open("POST", window.location.pathname, true); xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xmlhttp.send("&email=" + email + "&password=" + password + "&name=" + name + "&sex=" + sex + "&enabled=" + enabled + "&role=" + role + "&_csrf=" + csrf); } } function stateChange() { var success = document.getElementById("success"); if (xmlhttp.readyState == 4) { // 4 = "loaded" if (xmlhttp.status == 200) { // 200 = "OK" success.setAttribute("class", "text-success"); success.innerHTML = "注冊成功"; alert("注冊成功,點擊確定進行登錄") window.location.href = getContextPath() + "/user/information" } else if (xmlhttp.status == 409) { success.setAttribute("class", "text-danger"); success.innerHTML = "用戶郵箱已存在"; } else if (xmlhttp.status == 500) { success.setAttribute("class", "text-danger"); success.innerHTML = "服務器可能出現了問題"; } } }
Controller
Controller 是 Model 和 View 的粘合劑。Model 的增刪改查的操作由 Controller 負責,View 的顯示由 Controller 負責。Controller 實質上是 Java EE 的 Servlet。
在 Spring MVC 中,首先配置相關 DispatcherServlet,之后再編寫 Controller。
package per.piers.onlineJudge.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.view.InternalResourceViewResolver; @Configuration @EnableWebMvc @ComponentScan("per.piers.onlineJudge.controller") public class WebConfig extends WebMvcConfigurerAdapter { @Bean public ViewResolver viewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/jsp/"); resolver.setExposeContextBeansAsAttributes(true); resolver.setSuffix(".jsp"); return resolver; } @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } }
package per.piers.onlineJudge.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; import per.piers.onlineJudge.model.User; import per.piers.onlineJudge.util.DataAccessObject; import per.piers.onlineJudge.util.ExcelUtil; import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; @Controller @RequestMapping("/testManager") public class UserImportController { private DataAccessObject dao; @Autowired public UserImportController(DataAccessObject dao) { this.dao = dao; } @RequestMapping("/import/user") public String importUser() { return "import/user"; } @RequestMapping(path = "/import/user", method = RequestMethod.POST) public String importResult(@RequestPart("usersFile") MultipartFile usersFile, HttpServletRequest request, Model model) throws IOException { String path = request.getSession().getServletContext().getRealPath("/") + "/tmp/" + usersFile.getOriginalFilename(); File file = new File(path); file.getParentFile().mkdirs(); file.createNewFile(); usersFile.transferTo(file); ExcelUtil excelUtil = new ExcelUtil(); HashSet<String> emails = excelUtil.readColumns(file, "用戶郵箱"); try { if (emails == null) { model.addAttribute("failure", "讀取列用戶郵箱出錯,可能是沒有列用戶郵箱"); } else { User selectUser = new User(); selectUser.setEmail(((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()); Integer uidAdmin = dao.selectUser(selectUser).getId(); HashMap<String, String> status = dao.importUser(emails, uidAdmin); StringBuilder builder = new StringBuilder(); for (String key : status.keySet()) { builder.append(String.format("%s,%s\n", key, status.get(key))); } model.addAttribute("success", builder.toString()); } } catch (Exception e) { model.addAttribute("failure", e.getMessage()); } finally { return "import/result"; } } }
Spring 技術:這里的 Controller 是由 Spring MVC 提供的。本系統還設計了一個 ErrorController,用戶異常處理的 Controller,返回錯誤的頁面。
package per.piers.onlineJudge.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class ErrorController { @RequestMapping(path = "/error/401") public String error401() { return "error/401"; } @RequestMapping(path = "/error/403") public String error403() { return "error/403"; } @RequestMapping(path = "/error/404") public String error404() { return "error/404"; } @RequestMapping(path = "/error/409") public String error409() { return "error/409"; } @RequestMapping(path = "/error/500") public String error500() { return "error/500"; } }
Spring 技術:異常的捕獲和處理是由標有 @ControllerAdvice 注解的類處理,需要定義捕獲的異常類型、如何處理(返回的 HTTP 狀態碼,返回的頁面)。
package per.piers.onlineJudge.controller; import org.apache.ibatis.exceptions.PersistenceException; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import per.piers.onlineJudge.Exception.CRUDException; import per.piers.onlineJudge.Exception.ExistenceException; import per.piers.onlineJudge.Exception.ExpiryException; import javax.mail.MessagingException; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; @ControllerAdvice public class GlobalExceptionHandler { @ResponseStatus(value = HttpStatus.UNAUTHORIZED) @ExceptionHandler(BadCredentialsException.class) public String badCredentialsExceptionHandler(Exception e, Model model) { model.addAttribute("exception", getExceptionMessage(e)); return "error/401"; } @ResponseStatus(value = HttpStatus.FORBIDDEN) @ExceptionHandler(value = {ExpiryException.class, IllegalArgumentException.class}) public String illegalStateExceptionHandler(Exception e, Model model) { model.addAttribute("exception", getExceptionMessage(e)); return "error/403"; } @ResponseStatus(value = HttpStatus.CONFLICT) @ExceptionHandler(ExistenceException.class) public String existenceExceptionHandler(Exception e, Model model) { model.addAttribute("exception", getExceptionMessage(e)); return "error/409"; } @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(value = {CRUDException.class, IOException.class, IllegalStateException.class, MessagingException.class, PersistenceException.class}) public String CRUDExceptionHandler(Exception e, Model model) { model.addAttribute("exception", getExceptionMessage(e)); return "error/500"; } public String getExceptionMessage(Exception e) { StringWriter stringWriter = new StringWriter(); PrintWriter printWriter = new PrintWriter(stringWriter); e.printStackTrace(printWriter); return stringWriter.toString(); } }