Spring框架面经。包括mubatis、spring、springMVC、springBoot,以理解为主。
1 介绍一下vhr人力资源管理系统
vhr 是一个前后端分离的人力资源管理系统,包括人事管理、系统管理等模块,基于 Restful 风格开发后端接口、使用 Swagger 进行接口测试;
技术栈:Spring Boot、Spring Security、MyBatis、MySQL、Redis、RabbitMQ
- 1、使用 Spring Security 基于角色的动态访问控制,实现用户认证和用户授权;即根据用户的角色赋予不同的操作权限和显示菜单。
- 2、将邮件服务划分为单独的模块、引入消息中间件 RabbitMQ,实现异步发送欢迎新员工的入职邮件;
- 3、实现前后端分离,后端不再处理页面跳转仅传递 JSON 字符串,实现了基于 JWT 的无状态登录;
2 vhr的数据库是怎么设计的?
面向的用户是公司的管理人员,然后实现了基于角色的动态动态访问控制,基本的就有管理人员的hr表(用户表)、role表(角色表)、menu表(菜单表),然后就是实现用户、角色和菜单之间连接关系的hr_role表(一对多的关系)、menu_role表(多对多的关系)。然后就是其他一些公司的employee表(员工表)、departmen表(部门表)、joblevel表(职称表)等。
- hr用户表,公司的管理人员也就是该系统的使用用户。。一个用户一般拥有一种或者多种角色
- role角色表,不同的角色对用有不同的权限,比如人事专员只管人事、部门经理管理该部门的员工等
- hr表和role表通过 一个hr_role表相联系,即指定一个用户具有哪几种角色
- 还有一个menu表,相当于是一个资源表,menu中的数据会以json返回给前端,然后vue动态更新。其中里面最重要的字段就是url、路径匹配规则,所有的请求会被拦截然后根据这个url去查询访问这个url需要什么角色的权限,然后看当前用户是否具备相应的角色。
- menu_role表将menu和role表相联系,即指定某个url会对哪些角色可见(menu和role是多对多的关系)
3 RESTful风格,介绍一下
3.1 RESTful风格是什么?
URI统一资源标识符
URL是统一资源定位符
REST(Representational State Transfer) 指 表现层状态转化。
在web应用中,我们一般用URI统一资源标识符来表示一种资源
表现层指的是一个”资源“的具体表现形式,比如一个html文本、一个xml或者一个图片都是表现层
状态转换就是客户端通过http的几种请求方式(get\post\put\delete)对服务器进行访问,完成资源的状态转换
- GET 用来获取资源、POST 用来新建资源(也可以用于更新资源)、PUT 用来更新资源、DELETE 用来删除资源
REST的核心在于,当你设计一个系统的时候,资源是第一位的考虑,你首先从资源的角度进行系统的拆分、设计,而不是像以往一样以操作为角度来进行设计。
总的来说就是,RESTful定义了一种客户端和服务器交互的风格,使URL更简洁、更有层次感。
3.2 RESTful的使用示例
SpringMVC 对 RESTful 提供了非常全面的支持,主要有如下几个注解:
- @PathVariable注解:提取请求地址中的参数
- @PostMapping注解:映射一个POST请求
- @GetMapping注解:映射一个GET请求
- @PutMapping注解:映射一个PUT请求
- @DeleteMapping注解:映射一个DELETE请求
1 使用@PathVariable 注解可以将Controller下方法的参数绑定到URL的模板变量上,使url的访问更灵活。反之如果不使用的话,就需要在URL手动用=赋值参数
2 使用不同的注解组合(不同的请求方式)可以实现 相同的URL实现不同的效果!!使显示的url更简洁和有层次感
3.3 RESTful风格的设计原则
URL尽量使用名词复数, 不使用动词
访问同一个URL地址, 采用不同的请求方式, 代表执行不同的操作(get获取, post新增等)
服务器返回的响应格式数据,尽量通过XML JSON进行数据传递;
无状态连接,服务器端不应保存过多上下文状态,即每个请求都是独立的;
4 前后端分离时,前后端是如何交互的
4.1 后台如何处理前端传递过来信息,并返回数据的过程
其实就是问springMVC的原理和执行流程
用户发出请求后,前端传过来的实际上是一个url,后台接受请求,返回数据给前端。接受请求并返回数据其实就是springMVC的整个处理流程。
SpringMVC的执行过程是围绕着前置(调度)控制器DispatcherServlet的调度来设计的。
- 第一步,DispatcherServlet拦截请求,并调用 处理器映射HandlerMapping 和 解析控制器映射HandlerExecution。
- 目的是根据url查找对应控制器,找到是哪个Controller应该处理这个请求,找到之后返回给DispatcherServlet
- 第二步,DispatcherServlet调用HandlerAdapter处理器适配器,并让他调用那个Controller。
- 接下来就是我们编写的代码逻辑,后端Controller调Service、Service调Mapper接口,mapper.xml做持久层的增删改查,得到一个JSON对象传递给前端
- 前端解析JSON对自己的js对象,然后得到视图和模型,通过HandlerAdapter处理器适配器传递给DispatcherServlet
- 第三步,DispatcherServlet调用视图解析器(ViewResolver)来解析逻辑视图,最后得到视图,再呈现给用户!!
总结:简单来讲的话就是三步:1通过url找到对应的Controller、然后2调用执行持久化层的查询获得逻辑视图或者model,最后3解析视图、并呈现给用户。
4.2 后端返回值有字符串、JSON、实体对象,如何统一的传给前端的?
做一个删除操作——返回的是RespBean对象
查询所有员工 返回的是一个RespPageBean对象
查询部门的时候返回的是一个list集合 List
@ResponseBody是作用在方法上的,@ResponseBody 表示该方法的返回结果直接写入 HTTP response body 中
其实就是SpringBoot框架的功能,使用了@ResponseBody注解,所有的java对象都转换为了JSON格式写入Http response body中。
所以我后端不用管其他,你要集合我返回集合、要msg我返回msg、要page返回page,反正最后都变成JSON给前端了。
5 RBAC基于角色的权限访问控制
5.1 基于角色的动态访问控制逻辑
使用springScurity完成用户认证和用户授权。@EnableWebSecurity //开启WebSecurity模式,然后编写一个SecurityConfig类继承WebSecurityConfigurerAdapter,重写里面的configure方法给用户设置角色和权限。
因为是前后端分离的项目,基本的登录逻辑就是:用户(hr表)登录成功之后,可以通过hr_role表查询到用户的角色,再根据用户角色去(meau_role中)查出来用户可以操作的菜单(资源),然后把这些可以操作的资源,组织成一个 JSON 数据,返回给前端,前端再根据这个 JSON 渲染出相应的菜单。
这样就可以根据数据库中的表信息,动态的配置资源-角色以及用户-角色之间的关系,进而调整用户可以操作的资源(菜单)。
5.2 有状态登录和无状态登录
传统的有状态登录Session
服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理。典型的就是Session。
例如登录:用户登录后,我们把用户的信息保存在服务端 session 中,并且给用户一个 cookie 值,记录对应的 session,然后下次请求,用户携带 cookie 值来(这一步有浏览器自动完成),我们就能识别到对应 session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:
服务端保存大量数据,增加服务端压力
服务端保存用户状态,不支持集群化部署

RESTful 风格的无状态服务
微服务集群中的每个服务,对外提供的都使用 RESTful 风格的接口。而 RESTful 风格的一个最重要的规范就是:服务的无状态性,即:
- 服务端不保存任何客户端请求者信息
- 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份
好处:
多次请求不需要必须访问到同一台服务器,
服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)
减小服务端存储压力
使用JWT实现无状态服务
- 首先客户端发送账户名/密码到服务端进行认证登录
- 认证通过后,服务端将用户信息加密并且编码成一个 token,返回给客户端
- 以后客户端每次发送请求,都需要携带认证的 token Json web token (JWT)
- 服务端对客户端发送来的 token 进行解密,判断是否有效,并且获取用户登录信息

5.3 Token如何保存状态
如何处理注销账户之后token还有效的问题?
相关的有:退出登录、修改密码、修改权限、注销账户之后,token还有效
Redis用来储存token,账户注销或者退出登录之后就删除Redis中的token
这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。
token 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。
- 解决方案一:将 token 存入内存数据库:将 token 存入 DB 中,redis 内存数据库在这里是是不错的选择。如果需要让某个 token 失效就直接从 redis 中删除这个 token 即可。但是,这样会导致每次使用 token 发送请求都要先从 DB 中查询 token 是否存在的步骤,而且违背了 JWT 的无状态原则。
token 有效期一般都建议设置的不太长,那么 token 过期后如何认证,如何实现动态刷新 token,避免用户经常需要重新登录?
设置token的有效期为30分钟,服务器每次校验时,检查token的有效期,如果快过期了,就重新生成一个token给客户端。客户端请求之前先检查更新自己的token
先来看看在 Session 认证中一般的做法:假如 session 的有效期30分钟,如果 30 分钟内用户有访问,就把 session 有效期被延长30分钟。
- 类似于 Session 认证中的做法:这种方案满足于大部分场景。假设服务端给的 token 有效期设置为30分钟,服务端每次进行校验时,如果发现 token 的有效期马上快过期了,服务端就重新生成 token 给客户端。客户端每次请求都检查新旧token,如果不一致,则更新本地的token。这种做法的问题是仅仅在快过期的时候请求才会更新 token ,对客户端不是很友好。
- 每次请求都返回新 token :这种方案的的思路很简单,但是,很明显,开销会比较大。
- token 有效期设置到半夜 :这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。
- 用户登录返回两个 token :第一个是 acessToken ,它的过期时间 token 本身的过期时间比如半个小时,另外一个是 refreshToken 它的过期时间更长一点比如为1天。客户端登录后,将 accessToken和refreshToken 保存在本地,每次访问将 accessToken 传给服务端。服务端校验 accessToken 的有效性,如果过期的话,就将 refreshToken 传给服务端。如果有效,服务端就生成新的 accessToken 给客户端。否则,客户端就重新登录即可。该方案的不足是:1⃣️需要客户端来配合;2⃣️用户注销的时候需要同时保证两个 token 都无效;3⃣️重新请求获取 token 的过程中会有短暂 token 不可用的情况(可以通过在客户端设置定时器,当accessToken 快过期的时候,提前去通过 refreshToken 获取新的accessToken)。
==5.4 JWT的格式??==
6 邮件服务原理
vhrserver相当于只是发送了一个消息给中间件
然后真正发送邮件的功能都在mailserver中,JavaMailSender接口
将邮件服务划分为单独的模块、引入消息中间件 RabbitMQ,实现异步发送欢迎新员工的入职邮件;即当管理人员比如hr在界面上操作新增了一名员工到数据库中,就会自动的向中间件RabbitMQ发送一条消息,然后mailServer会自动异步的订阅消息,向新入职的员工发送一封入职邮件,里面包含姓名、职位、职称和部门等信息。
vhrServer中的业务逻辑:
- 新增员工的命令返回值为1时,进入邮件发送的代码中
- 指定消息的唯一UUID,获取一个新的employee对象用于传递
- 一个MailSendLog类用来记录消息的发送日志,里面实例变量有消息的id、员工id、投递状态、投递规则key exchage等
- 配置RabbitMQ的配置文件,声明队列、交换机、绑定交换机等
mailServer中的业务逻辑:
- 配置RabbitMQ的配置文件,然后消费消息就ok了
- 消息确认机制改为手动,只要消费成功一条消息,就将消息id记录在Redis上,避免消息的重复消费
7 RabbitMQ如何保证消息的可靠性
1 保证我们的消息能够成功到达队列
消息传递、要经过三个流程:
1 消息到达交换机
- 第一个流程可能由于网络的波动、延迟等原因,消息没有准确达到交换机
2 交换机把消息路由到队列中
- 第二个流程可能routing key错误等原因,消息没有准确到达队列
3 消息持久化写入日志
- 第三个流程可能在消息写入日志的过程中,broker crash掉了,重启后消息丢失
解决方案一:事务控制,但大幅度降低了效率不推荐使用
解决方案二:发送者确认模式:即消息发送失败回调、路由失败回调
1 通过实现ConfirmCallBack接口,消息发送到交换器Exchange后触发回调
2 通过实现ReturnCallback接口,如果消息从交换器发送到对应队列失败时触发(比如根据发送消息时指定的routingKey找不到队列时会触发)
总结:
如果成功入列,单纯给客户端发送”ack”信号
如果消息无法路由到队列则是发送 “消息” + “ack”,且”消息”比ack信号先到
2 rabbitmq的持久化(解决消息到达队列之后的可靠性)
三个持久化:交换机持久化、队列持久化、消息持久化
持久化的消息在进入持久化队列之后会写入到rabbitmq的持久性日志文件中,消息被消费掉后,会把持久性日志文件中该消息标记为等待垃圾收集
三个持久化很大程度上已经解决了rabbitmq这一端因为宕机而导致消息丢失的问题。。。如果在broker在把消息写入日志文件的时候崩掉了(极端情况),这时可以借助mirror queue来处理
3 确保一条消息只消费一次
首先将 RabbitMQ 的消息自动确认机制改为手动确认,然后每当有一条消息消费成功了,就把该消息的唯一 ID 记录在 Redis 上,然后每次收到消息时,都先去 Redis 上查看是否有该消息的 ID,如果有,表示该消息已经消费过了,不再处理,否则再去处理。
这里使用的是Token的思想,核心就是每个操作都有一个唯一凭证 token,一旦执行成功,对于重复的请求,总是返回同一个结果。
每一个邮件消息都有唯一的uuid,作为唯一的凭证