入门项目——学生成绩管理系统(demo)
发表于:2022-08-08 | 分类: 项目
字数统计: 4.6k | 阅读时长: 23分钟 | 阅读量:

学生成绩管理系统

项目环境

jdk 1.8以上即可

maven 3.6.1以上

mysql 8

idea 2021

JQuery

项目创建

新建选择spring Initializr

image-20220808191354776

指定项目名,路径,类型等信息。

image-20220808191603691

指定导入的jar包,

spring web为前后端连接

image-20220808191707878

在SQL下选择mybatis的jar包与sql的驱动,点击完成,等待一段时间,下载模板后,创建完成。

一般来说,idea会对每一个新建的项目进行一些类初始化的操作,例如依赖项等,因此我们往往需要等待一段时间,第一次可能会格外的慢。

数据库

建一个简单表,以后扩展。

image-20220808192526607

数据库连接,我选择新建一个yaml文件

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/milk?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

测试数据库连接

image-20220808193722466

package top.rczmm.studentmanager;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.SQLException;

@SpringBootTest
class StudentManagerApplicationTests {

    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() {
    }

    @Test
    void getConnection() throws SQLException {
        System.out.println(dataSource.getConnection());
    }

}

如果是idea可能在自动装配出报错,但是不用理他,直接运行,也可以在设置中将报错改为警告。

测试结果,输出连接池image-20220808193850646

表示成功。

静态资源

静态资源包括网页、js、css、图片等内容。

image-20220808194009182

规定放在resources目录下(可以去看源码,但不建立此处个性化)。

其中,static为默认根目录,可以放任何资源,并且可以通过url直接访问(不用/static/)。

templates需要thymeleaf模板引擎,且无法直接被访问。

<dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf</artifactId>
            <version>3.0.15.RELEASE</version>
        </dependency>

建立首页

image-20220808194516411

启动项目

image-20220808194540454

springboot的启动最为简单,直接运行启动类即可,啥都交给自动装配。

运行后再访问静态资源时,idea有时候可能会出bug,这是因为idea对js代码兼容较差出现的情况,有时候js不能正常的加载,此时可以通过清理缓存,maven项目的clean与install重新部署依旧rebuild重新构建等三种方式解决,当然也可以重启电脑。

注册

1、建立实体类(根据简单表)

package top.rczmm.studentmanager.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    int id;
    String name;
    char sex;
}

此处使用了三个注解,分别表示get与set方法以及无参和有参构造的生成,使用注解前,需要先在pom.xml里引入lombok包。

image-20220809101412761

要注意,student实体类是需要在网络中传输的,因此我们需要对它序列化,很简单,实现接口就可以。

package top.rczmm.studentmanager.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student implements Serializable {
    int id;
    String name;
    char sex;
}

2、持久层

也就是mybatis来操作数据库

package top.rczmm.studentmanager.Mapper;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import top.rczmm.studentmanager.pojo.Student;

@Mapper
public interface StudentMapper {

    @Insert("insert into student (id,name,sex) values (#{id},#{name},#{sex})")
    Integer insert(Student student);

    @Select("select * from student")
    Student selectAll();

    @Select("select * from student where id = #{id}")
    Student selectByID();
}

此处,在构建mapper接口时,我直接用注解写进sql语句,而不去编写xml映射文件,两种方式有利有弊,但是无疑,注解在写这种小demo时,更快。

3、测试

一般的,每一层写完我们都需要单元测试

这时候发现,表的字段设置有误,修改一下。

image-20220809102848382

image-20220809103049557

package top.rczmm.studentmanager.Mapper;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import top.rczmm.studentmanager.pojo.Student;

@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {

    @Autowired
    StudentMapper studentMapper;

    @Test
    public void insertStudent() {
        Student student = new Student(20220809,"小王八",'男');
        Integer rows = studentMapper.insert(student);
        System.out.println(rows);
    }

    @Test
    public void selectAll(){
        System.out.println(studentMapper.selectAll());
    }

    @Test
    public void selectByID(){
        System.out.println(studentMapper.selectByID(20200809));
    }


}

image-20220809103445768

测试通过,要注意此处的变量rows是受改变的行数,也就是说,当行数小于1时,代表sql语句执行失败。

4、业务层

接收前端数据

完成业务逻辑

4.1 规划异常

异常的出现很正常,注册时用户名被占用,输入不符合规范都是异常。

在处理异常时,不用笼统的用runtime运行时来定位,因此需要细分。

此处,建立一个业务异常基类,一个用户ID占用子类,一个插入异常子类,还可扩展,省略。

关于异常的定义也很简单,此处就是一个小demo,写一下构造就可以。

package top.rczmm.studentmanager.Service.ex;

public class ServiceException extends RuntimeException{
    
    public ServiceException() {
        super();
    }

    public ServiceException(String message) {
        super(message);
    }

    public ServiceException(String message, Throwable cause) {
        super(message, cause);
    }

    public ServiceException(Throwable cause) {
        super(cause);
    }

    public ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

也可以用lombok,我是为了水字数。

再写两个异常,重写父类的构造就可以。

package top.rczmm.studentmanager.Service.ex;

public class IDDuplicationException extends ServiceException{
    public IDDuplicationException() {
    }

    public IDDuplicationException(String message) {
        super(message);
    }

    public IDDuplicationException(String message, Throwable cause) {
        super(message, cause);
    }

    public IDDuplicationException(Throwable cause) {
        super(cause);
    }

    public IDDuplicationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}
package top.rczmm.studentmanager.Service.ex;

public class InsertException extends ServiceException{
    public InsertException() {
    }

    public InsertException(String message) {
        super(message);
    }

    public InsertException(String message, Throwable cause) {
        super(message, cause);
    }

    public InsertException(Throwable cause) {
        super(cause);
    }

    public InsertException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

4.2 设计接口

package top.rczmm.studentmanager.Service;

import top.rczmm.studentmanager.pojo.Student;

public interface StudentService {
    void regiect(Student student);
}

小demo,功能就写一个注册就行。

package top.rczmm.studentmanager.Service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import top.rczmm.studentmanager.Mapper.StudentMapper;
import top.rczmm.studentmanager.Service.ex.IDDuplicationException;
import top.rczmm.studentmanager.Service.ex.InsertException;
import top.rczmm.studentmanager.pojo.Student;


@Service
public class StudentServiceImpl implements StudentService{
    @Autowired
    StudentMapper studentMapper;

    @Override
    public void regiect(Student student) {
        Integer id = student.getId();
        Student result = studentMapper.selectByID(id);
        String name = student.getName();
        char sex = student.getSex();

        if (result != null){
            throw new IDDuplicationException("用户ID已经存在!");
        }

        Integer rows = studentMapper.insert(student);

        if (rows != 1) {
            throw new InsertException("插入时出现未知异常!");
        }

    }
}

写上实现类,可以写在一个目录下,多了就可以拆分。

注意此处的实现类,因为要交给spring管理,因此必须加上service注解。

4.3 测试

package top.rczmm.studentmanager.Service;


import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import top.rczmm.studentmanager.Service.ex.ServiceException;
import top.rczmm.studentmanager.pojo.Student;

@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentServiceTest {
    @Autowired
    StudentService studentService;

    @Test
    public void regiect(){
        try {
            Student student = new Student(20220802,"小扒菜",'女');
            studentService.regiect(student);
            System.out.println("OK");
        }catch (ServiceException e){
            System.out.println(e.getClass().getName());
            System.out.println(e.getMessage());
        }
    }

}

image-20220809110014533

5、控制层

5.1 响应

对于响应,一般的,我们都用状态码来描述,将这个功能封装到类里,将这个类作为方法的返回值给前端。

因为涉及到数据流,因此还是需要序列化。

package top.rczmm.studentmanager;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class JsonResult<E> implements Serializable {
    Integer state;
    String message;
    E data;
}

5.2 请求

package top.rczmm.studentmanager.Controller;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.rczmm.studentmanager.JsonResult;
import top.rczmm.studentmanager.Service.StudentService;
import top.rczmm.studentmanager.Service.ex.IDDuplicationException;
import top.rczmm.studentmanager.Service.ex.InsertException;
import top.rczmm.studentmanager.pojo.Student;

@RestController
@RequestMapping("student")
public class StudentController {

    @Autowired
    StudentService studentService;


    @RequestMapping("reg")
    public JsonResult<Void> reg(Student student){
        JsonResult <Void> jsonResult = new JsonResult<>();
        try {
            studentService.regiect(student);
            jsonResult.setState(200);
            jsonResult.setMessage("注册成功");
        }catch (IDDuplicationException e){
            jsonResult.setMessage("ID重复,无法注册");
            jsonResult.setState(2000);
        }catch (InsertException w){
            jsonResult.setState(5000);
            jsonResult.setMessage("出现未知异常!");
        }
        return jsonResult;
    }

}

当然,对于控制层的业务,我们应该建立一个基类,针对不同业务做多态的扩展,但是此处只是一个小demo就不做了。

6、前端页面

前端页面,使用ajax来异步加载请求。

此处不用原生,使用JQuery。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
</head>
<body>
<form id="form-reg" class="form-horizontal" role="form">
    <div class="form-group">
        <label class="col-md-3 control-label">ID :</label>
        <div class="col-md-8">
            <input name="id" type="text" class="form-control" placeholder="请输入ID">
        </div>
    </div>
    <div class="form-group">
        <label class="col-md-3 control-label"> 姓名:</label>
        <div class="col-md-8">
            <input name="name" type="text" class="form-control" placeholder="请输入姓名">
        </div>
    </div>
    <div class="form-group">
        <label class="col-md-3 control-label"> 性别:</label>
        <div class="col-md-8">
            <input name= "sex" type="text" class="form-control" placeholder="请输入性别">
        </div>
    </div>

    <div class="form-group">
        <label class="col-md-3 control-label"></label>
        <div class="col-md-8">
            <input id="btn-reg" class="btn btn-primary" type="button" value="立即注册" />
        </div>
    </div>
</form>
<script type="text/javascript">
    $("#btn-reg").click(function() {
        $.ajax({
            url: "/student/reg",
            type: "POST",
            data: $("#form-reg").serialize(),
            dataType: "json",
            success: function(json) {
                if (json.state == 200) {
                    alert("注册成功!");
                } else {
                    alert("注册失败!" + json.message);
                }
            },
            error: function (xhr){
                alert("未知错误"+xhr.status)
            }
        });
    });

</script>
</body>
</html>

7、运行测试

image-20220809111827983

测试是否会重复ID

image-20220809111856865

注册功能完成。

完善一下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
</head>
<body>
<form id="form-reg" class="form-horizontal" role="form">
    <div class="form-group">
        <label class="col-md-3 control-label">ID :</label>
        <input id="id" type="text" class="form-control" placeholder="请输入ID">
    </div>
    <div class="form-group">
        <label class="col-md-3 control-label"> 姓名:</label>
        <input id="name" type="text" class="form-control" placeholder="请输入姓名">
    </div>
    <div class="form-group">
        <label class="col-md-3 control-label"> 性别:</label>
        <select id="sex">
            <option value="男">男</option>
            <option value="女">女</option>
        </select>
    </div>

    <div class="form-group">
        <label class="col-md-3 control-label"></label>
        <input id="btn-reg" class="btn btn-primary" type="button" value="立即注册"/>

    </div>
</form>
<script type="text/javascript">
    $("#btn-reg").click(function (message) {
        const id = $("#id").val();
        const name = $("#name").val();
        const sex = $("#sex option:checked").val();
        if (id !== undefined || name !== undefined) {
            alert("内容不能为空!")
        } else {
            console.log(sex,name,id)
            $.ajax({
                url: "/student/reg",
                type: "POST",
                data: {"name":name,"sex":sex,"id":id},
                dataType: "json",
                success: function (json) {
                    if (json.state === 200) {
                        alert("注册成功!");
                    } else {
                        alert("注册失败!" + json.message);
                    }
                },
                error: function (xhr) {
                    alert("未知错误!" + xhr.status)
                }
            });
        }
    });

</script>
</body>
</html>

接下来写登录

登录

持久层

用户登录的本质是根据账号查询密码,因此我们在sql语句之前,再次更新最开始的简单表。

image-20220809165525367

sql语句开发,后台方法已经写完。

image-20220809165629992

此处使用注解,也无需配置xml映射。

业务层

规划异常

登录时异常较多,其中针对这个小demo,主要为密码错误,此处略过。

接口

编写登录方法

image-20220809170235090

实现登录方法

 @Override
    public Student login(int id, int password) {
        Student result = studentMapper.selectByID(id);
        if (result == null) {
//            此处应抛出异常
            System.out.println("用户不存在!");
        }

        return result;
    }

登录时,密码等应加密,此处小demo,没用。

测试

@Test
public void loginTest() {
    try {
        int id = 20220809;
        int password = 11111;
        studentService.login(id, password);
        System.out.println("OK");
    } catch (ServiceException e) {
        System.out.println("111");
    }
}

控制层

处理异常

首先处理登录功能,在业务层抛出的异常,此处未设置,略过。

设计请求

这一步是建立在注释内的一步,设计用户提交的请求,并且设计响应的方式。

包括但不限于,请求路径,请求参数,请求类型吗,相应结果等。

处理请求

@RequestMapping("login")
public JsonResult<Student> login(int id, int password){
    Student data = studentService.login(id, password);
    return new JsonResult<>(data);
}

要注意的,此处返回值new的对象需要添加对应的构造方法。

前端页面

<form id="form-login" action="index.html" class="form-horizontal" role="form">
    <!--用户名-->
    <div class="form-group">
        <label for="username" class="col-md-3 control-label">ID:</label>
        <div class="col-md-8">
            <input id="id" type="text" class="form-control" placeholder="请输入ID">
        </div>
    </div>
    <!--密码-->
    <div class="form-group">
        <label for="password" class="col-md-3 control-label"> 密码:</label>
        <div class="col-md-8">
            <input id="password" type="text" class="form-control" placeholder="请输入密码">
        </div>
    </div>

    <!--提交按钮-->
    <div class="form-group">
        <label class="col-md-3 control-label"></label>
        <div class="col-md-8">
            <input id="btn-login" class="btn btn-primary" type="button" value="登录" />
        </div>
    </div>
</form>
$("#btn-login").click(function() {
    $.ajax({
        url: "/student/login",
        type: "POST",
        data: $("#form-login").serialize(),
        dataType: "json",
        success: function(json) {
            if (json.state == 200) {
                alert("登录成功!");
                $.cookie("avatar", json.data.avatar, {expires: 7});
                console.log("cookie中的avatar=" + $.cookie("avatar"));
                location.href = "index.html";
            } else {
                alert("登录失败!" + json.message);
            }
        }
    });
});

拦截器

在MVC中,拦截请求是通过处理器拦截器器HandlerInterceptor来实现的,它拦截的目标是请求地址,即URL。在MVC在自定义一个拦截器,需要实现这个接口。

该拦截器有三大方法,在请求处理之前被调用的preHandle()以及在当前请求进行处理之后被调用的postHandle()和在整个请求结束之后的afterCompletion()。

添加拦截器

项目中很多操作都需要登录后才可以直接执行,如果在每个请求前都去写检查session有没有登录信息,是非常不现实的。

创建拦截器类

package top.rczmm.studentmanager.Interceptor;

import org.springframework.web.servlet.HandlerInterceptor;

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

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(request.getSession().getAttribute("id") ==null){
            response.sendRedirect("login.html");
            return false;
        }
        return true;
    }
}

要注意,在springboot项目中,自定义一些拦截器、分解器、转换器。在1.5版本之前,是依靠重写WebMvcConfigurerAdapter类的方法,2.0版本之后,该类过时,因此只能靠实现WebMvcConfigurer接口来实现。

创建拦截器的配置类并实现

package top.rczmm.studentmanager.config;


import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.rczmm.studentmanager.Interceptor.LoginInterceptor;

import java.util.*;

public class LoginInterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        HandlerInterceptor interceptor = new LoginInterceptor();

        List<String> patterns = new ArrayList<String>();

        patterns.add("");
        patterns.add("");
        patterns.add("");
        patterns.add("");
        patterns.add("");

        registry.addInterceptor(interceptor).addPathPatterns("/**").excludePathPatterns(patterns);
    }
}

此时,我们就可以重新构建login方法,在登录成功也就是账号和密码与数据库匹配之后,将id与password存入httpsession对象中。(注意将css、js、图片等公共资源加入白名单。)

AOP

spring很好的支持了AOP。

在处理业务时,假设存在一个切面,在切面中可以定义方法,那么就只需要配置好连接点,就可以在不修改原有数据处理流程的代码的基础之上,就可以使得若干个流程都执行相同的代码。

切面方法

访问是public、返回值类型任意,但是在使用@around时,必须使用Object类型,并且返回连接点方法的返回值,如果是@before或者@after等注解,就自定义。

统计业务时长

在使用AOP之前,需要先引入相关的包。

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
</dependency>
package top.rczmm.studentmanager.aop;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class TimeAspect {

    @Around("execution(* top.rczmm.studentmanager.Service.*.*(..))")
    public Object around(ProceedingJoinPoint point)throws Throwable {

        long start = System.currentTimeMillis();

        Object result = point.proceed();

        long end = System.currentTimeMillis();

        System.out.println("耗时:"+(end - start) + "毫秒!");

        return point.proceed();
    }
}
上一篇:
基于python的QT(一)
下一篇:
高等数学