• Online Judge(OJ)搭建——3、MVC架构


    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();
        }
    
    }
  • 相关阅读:
    区分replace()和replaceAll()
    error:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Long
    关于设计SQL表的一些问题
    SQL语句的学习理解
    error:安装手电筒程序后在打开程序后报错:你的camera flashlight正在被其他程序占据
    Error:Execution failed for task ':app:processDebugGoogleServices'. > No matching client found for package name 'com.fortythree.sos.flashlight'
    Error:Error converting bytecode to dex: Cause: com.android.dex.DexException: Multiple dex files define Lcom/google/android/gms/internal/adp;
    Android Studio 封装的类的继承
    Android SDK安装
    MongoDB探索之路(二)——系统设计之CRUD
  • 原文地址:https://www.cnblogs.com/Piers/p/6942430.html
Copyright © 2020-2023  润新知