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="[dw]+" 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 ", 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(); } }