学生成绩管理系统
项目环境
jdk 1.8以上即可
maven 3.6.1以上
mysql 8
idea 2021
JQuery
项目创建
新建选择spring Initializr
指定项目名,路径,类型等信息。
指定导入的jar包,
spring web为前后端连接
在SQL下选择mybatis的jar包与sql的驱动,点击完成,等待一段时间,下载模板后,创建完成。
一般来说,idea会对每一个新建的项目进行一些类初始化的操作,例如依赖项等,因此我们往往需要等待一段时间,第一次可能会格外的慢。
数据库
建一个简单表,以后扩展。
数据库连接,我选择新建一个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
测试数据库连接
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可能在自动装配出报错,但是不用理他,直接运行,也可以在设置中将报错改为警告。
测试结果,输出连接池
表示成功。
静态资源
静态资源包括网页、js、css、图片等内容。
规定放在resources目录下(可以去看源码,但不建立此处个性化)。
其中,static为默认根目录,可以放任何资源,并且可以通过url直接访问(不用/static/)。
templates需要thymeleaf模板引擎,且无法直接被访问。
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.15.RELEASE</version>
</dependency>
建立首页
启动项目
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包。
要注意,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、测试
一般的,每一层写完我们都需要单元测试
这时候发现,表的字段设置有误,修改一下。
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));
}
}
测试通过,要注意此处的变量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());
}
}
}
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、运行测试
测试是否会重复ID
注册功能完成。
完善一下。
<!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语句之前,再次更新最开始的简单表。
sql语句开发,后台方法已经写完。
此处使用注解,也无需配置xml映射。
业务层
规划异常
登录时异常较多,其中针对这个小demo,主要为密码错误,此处略过。
接口
编写登录方法
实现登录方法
@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();
}
}