Spring Boot 初体验:为了课设

这学期的数据库课程设计,在我一通安利之后,我们小组接受了使用前后端分离的架构模式。


对我来说,这是我从Java基础之后直接转向Web开发的一次体验,说来惭愧,大学生涯过半,这才是我第一次开发一个完整的项目,也是第一次将流行的现代化开发技术运用到实际的项目中。

其实按照Java Web技术栈的学习路径来说,我这种“不胫而走”的学习过程实在过于跳跃了,但是好在我有幸找到了还算不错的资料,这种“一步登天”并没有让我遇到很大的阻碍,在功能的需求确定之后,整个后端的搭建与开发过程还是比较顺利的,期间也新学了一些多方面的技术与工具的使用。

接下来我想叙述一下自己从创建新项目开始到最终写完接口文档过程中的几个关键点,以便给未来要构建新项目的自己一个参考与总结。

从新建项目开始

借助全宇宙第一的IDE —— 喷气大脑JetBrains的IDEA,通过自带的Spring Initializer就能够十分轻松地创建Spring Boot项目了,不那么简单的其实是完全不清楚那一大堆依赖项都有什么作用,也不知道一个简单的项目首先需要用到哪些依赖。实话说,在私下经历了几次重开之后我才算琢磨明白要用到哪些依赖,也算是规划好项目的层次结构了。

万物起源:登陆

对所有的信息管理系统来说,登录功能几乎是这类软件功能的基石,所有的用户角色、权限,所有的实体功能都要建立在用户一登录的基础上。如果没有一套较为完整的登录流程,整个系统便无从运行。

在前几个学期,我做过一个要求不高的课设,那时候还没有要求设计UI,也没有任何数据库,只是简简单单地通过C++标准库的 fstream把信息写进文件,想实现登录只需要做一个判断即可。

1
if(username == input_name && password == input_password)// 判断用户名和密码是否匹配

但是现在不一样,现在有数据库,每当前端传来用户名和密码,都需要通过用户名和密码向数据库查询是否存在该用户,同时考虑到用户离职,但是不能够简单地将其从数据库中删除,这样会影响到后续的的数据统计,因此需要另设置用户的“在职”识别码,说白了就是个boolean变量,进而通过判断该用户是否在职来确定是否允许该登录请求通过。而且通常情况下咱们登录一个网站后,这个登录状态其实是有期限的,过期后就需要重新登录,这说明我们需要一个地方来保存下每个登录用户的登录状态,这个登录状态包含了用户的非敏感的登录信息,并且这个登录状态必须是唯一的,以此来保证这个登录状态不会被重复利用。

这种场景有一个专门的名字:跨域身份验证。其中比较主流的方式是JWT,即JSON Web Token:在前端发起登录请求后,服务端会通过登录信息来构建一串token作为该用户本次登陆的唯一凭证,这个token会被前端保存下来,之后的每次请求中都会携带token,由服务端来校验token是否合法,进而决定是否要执行功能代码。而这个token会采用一定的认证算法将携带的信息载荷加密形成签名,一旦载荷被修改,签名则立即发生变化,这样服务端获取token后只需通过载荷与约定的认证方式来计算出签名进行比对即可。

具体到JWT的实现上,虽然自己从头写一个工具类不算困难,但事实上有许多细节是需要考虑的,而且一个成熟的工具类需要后续不间断的维护和更新,所以本着非必要不造轮子的原则,比较著名的JJWT和 Java-JWT包都是不错的选择。在使用了JJWT后,我个人还是比较推荐Java-JWT,感觉它的限制会少一些,可以将token验证流程控制得更加精细。

城门卫士:拦截器

所谓拦截器,在这个小项目中最主要的作用是拦截所有登录之外的请求,只有用户登录情况正确后才能执行相应的功能。在Spring Boot中,SpringMVC已经为我们提供了拦截器,称为HandlerInterceptor,其下有三个方法声明,分别为 preHandlepostHandleafterCompletion,分别用于在Controller方法执行前、Controller方法执行后而 ModelView渲染前、ModelView渲染后。由于项目中只用到了preHandle,因此先简单介绍一下这个方法。

方法包含3个参数:

1
HttpServletRequest request, HttpServletResponse response, Object handler

request是当前服务端收到的请求

response是当前的响应

handler可以认为是Controller方法的整个方法签名

接着描述一下拦截的逻辑:首先我们需要实现HandlerInterceptor类并重写preHandle方法,在方法体中,首先获取token:

1
String token = request.getHeader("Authorization");

去具体判断何时应当认为token是符合约定的,即什么条件下允许服务端执行用户请求的功能。

随后,需要针对新增的拦截器添加配置,这一点通过Spring Boot的配置类来实现,我们需要做的有:

  1. 实现WebMvcConfigurer接口

  2. 重写addInterceptors方法

  3. 方法中添加配置代码:

    1
    2
    3
    4
    5
    6
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(loginInterceptor)
    .addPathPatterns("/**") // 拦截路径
    .excludePathPatterns("/login"); // 排除路径
    }

这样,拦截器便可以根据路径启用对应的拦截。

和而不同:跨域

什么是跨域?当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。而在我们的课设项目中,前端与后端的端口分别为8080和8888,因此当前端发出一个请求时,请求的url端口号是8888,而当前页面url的端口号则是 8080,这就形成了跨域。

浏览器默认是不允许跨域的,这源于浏览器的同源策略。

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

先不用关心如果没有同源策略的限制将会使前后端交互出现什么安全性问题,现在只需要知道同源策略能够在一定程度上规避一些危险,但却使开发人员需要额外考虑跨域问题,而跨域解决方案的执行对象则比较宽松,既可以由前端来做,也可以由后端来实现。而项目中前端的数据渲染是重中之重,因此就决定将所有看不见的部分都交给后端来考虑。

在Spring Boot中,由于它已经为我们定义好了相关的接口,因此我们需要做的有:

  1. 实现WebMvcConfigurer接口

  2. 重写addCorsMappings方法

  3. 在方法中添加配置代码

    1
    2
    3
    4
    5
    6
    registry.addMapping("/**")
    .allowedOriginPatterns("*")
    .allowCredentials(true)
    .allowedMethods("*")
    .allowedHeaders("*")
    .maxAge(3600);

小插曲

但是事实上,当我做完这一切以为只剩下前端负责的工作时,功能测试立马就出了问题:由于我没有前端来做真实测试,只好通过Postman来做请求测试后端功能,全无问题,然而但前端完成渲染后开始测试时却出现了接连不断的500 error,大家伙儿对着浏览器F12瞪了半天没看出来究竟是哪里的问题。很无奈,只能在百度上瞎猫碰上死耗子一般地搜索关键词,想不到还真能碰上记录了相同问题的文章,在这里感谢文章springboot拦截器导致@CrossOrigin失效

原因在于:CORS复杂请求时会首先发送一个OPTIONS请求做嗅探,来测试服务器是否支持本次请求,请求成功后才会发送真实的请求;而OPTIONS请求不会携带数据,导致这个请求被拦截了,直接返回了状态码,响应头中没携带解决跨域问题的头部信息,出现了跨域问题。

知道了原因,那么解决方法其实在非常简单,只需要在拦截器中首先检查请求的方法是否为OPTIONS即可:

1
2
3
//由于配置了跨域,如果拦截的是预检的OPTIONS请求,则放行
if (request.getMethod().equalsIgnoreCase("OPTIONS"))
return true;

天下太平:统一异常处理

在前后端分类的项目中,前后端的通信就都是依靠请求和响应报文来实现,因为关于计算机网络底层的那些通信协议由操作系统和框架帮助我们搭好了脚手架,所以站在业务开发的角度,我们只需要考虑以怎样的统一格式来规范请求和响应报文。

在我们的项目中,对于请求体而言,由于Spring Boot提供的@RequestBody注解能够将JSON字符串封装为定义好的类,因此只需要针对一个或多个功能请求设计一个对应的实体请求类:

例如对于登录功能而言,请求体中必然包含userID以及password字段,因此就可以定义LoginRequest类:

1
2
3
4
5
6
7
8
9
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginRequest {

private String userID;
private String password;

}

Controller响应的方法参数中获取这个类即可:

1
2
3
4
@PostMapping("/login")
public ResponseResult<Object> login(@RequestBody LoginRequest loginRequest) {
// 从loginRequest中获取信息执行登录操作
}

而响应体则不像请求体那么多变,响应体可以抽象为具有统一格式的实体类,由响应码消息数据三部分组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResponseResult<T> {

private Integer code; // 响应码,类似与HTTP code,但能够传递更加明确的信息。
private String msg; // 响应码代表的消息,前端可以通过响应码和提示消息共同判定用户请求是否存在异常,

private T data; // 由于要传输的数据类型会根据需要的不同而变化,因此采用泛型。

/* 包含部分参数的构造方法 */

}

说了这么多,其实都是在为统一异常处理设下铺垫。我们时长能看到有时候网页提示各种错误,比如404500502……当然这些基本上都是后端的服务器本身出了问题,但是在实际的场景中,服务端代码抛出的异常并非都由系统异常引起,相反,大部分异常都是由不合理的请求或者错误的代码逻辑引起的,而这些异常如果直接返回给前端甚至用户,用户并不知道究竟是什么地方出了问题,本着自己的锅自己背的原则,统一异常处理就体现得很有必要而且格外优雅了。

在Spring Boot中,存在专门的异常处理机制,通过在类上添加@ControllerAdvice注解告诉 Spring这是一个异常处理类,可以捕获项目中抛出的所有异常,在异常处理方法中,可以通过添加@ExceptionHandler注解来声明该方法用于处理哪种异常。

具体到课设项目,在拦截器中,如果token验证不通过,则会直接抛出异常:

1
throw new RuntimeException("Invalid Token");

在异常处理流程中,通过判断异常所携带的信息是否与所期望的信息相同来决定向前端返回怎样的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ControllerAdvice
public class LoginControllerAdvice {

@ResponseBody
@ExceptionHandler(RuntimeException.class)
public ResponseResult<Object> loginControllerAdvice(RuntimeException e) {
// 获取异常信息,如果与约定的异常信息相等则认为是登录异常,返回响应体,否则直接返回异常信息
String message = e.getMessage();
if (message.equals("Invalid Token"))
return new ResponseResult<>(10401, "Invalid Token");
return new ResponseResult<>(message);
}
}

交流共享:接口文档

写到这里,技术上想说的大部分都说完了,接口文档主要是为了方便前端开发,将所有接口的JSON规范以实例的形式展现出来,这样前端则只需要根据JSON格式来构造请求和渲染视图,而在测试出现问题时,前后端联调也更容易定位到bug所在的接口。在这一点上,开发过程中我是深有体会的,而事实上我写的接口数量要比实际上前端用到的要多,因此这份文档甚至在些报告的时候也帮助我确定了哪些接口没有用过,即不需要在报告中体现的部分。

总结

以上便是Spring Boot给我留下的第一印象了,不愧是Java Web领域的头把交椅,更新的速度也是飞快。

当然,我这种“跳级”的学习方式是绝对不推荐的,要不是为了能够不用每次课设都挖新坑,我肯定更愿意去了解一下计算机语言本身的魅力,毕竟人们都说程序员的三大浪漫是编译原理、图形学和操作系统,这一学期算是一次性摸遍了这三个领域,操作系统对我而言没有太多感触,可能是因为我学得不认真吧,图形学用的是古老的GLUT图形库,说实话就是——没什么好说的,而编译器的魅力则是吸引我很久了,只是手造语法分析总是让我感觉不得要领。好了说回来,Java Web的学习路线还是有些坡度的,对于想走这Java Web这条路的初学者,我从一个同为初学者角度来说还是建议从HTTP、Servlet等基础开始学习,毕竟根基不牢易遭反噬不是吗?


Spring Boot 初体验:为了课设
https://skycurtain.github.io/2022/01/08/springboot-foretaste-for-course-design/
作者
Skycurtain
发布于
2022年1月8日
许可协议