写在前面
在学习苍穹外卖过程中,弹幕常有 “为什么我打不开?为什么我没有输出?”的疑问,针对这些我也在学习过程中同样遇到的问题,万分感激在弹幕中找到了答案,并作出这系列汇总。本文内容是基于弹幕对苍穹外卖项目的实施与补充,仅供学习与分享之用,如有侵权请联系删除~
2024-07-04
目录
EmployeeService & EmployeeServiceImpl :save()实现逻辑
测试:
校验JWT步骤:
参数名称是啥?
参数值如何获取呢?
原因:
处理:
代码:
思考:为什么不在异常前先判断用户是否存在?
思考:如何获取当前用户的id?
思考:获取到id后,在拦截器我们并没有调用service,如何将id传给service操作?
如何验证代码是否在同一线程执行?
BaseContext.java类
为什么要封装为BaseContext?
设置EmployeePageQueryDTO封装前端传来的数据
为什么要定义该类?
EmployeeService & EmployeeServiceImpl:pageQuery实现逻辑:
使用PageHelper插件导:在pom文佳导入
EmployeeServiceImpl代码
Page对象提供的方法:
mapper.java代码
EmployeeMapper.xml代码:
没有出现红、蓝小鸟如何解决?
解决方案
方案①:修改路径src/main/java/com/sky/entity/Employee.java
方案②:扩展SpringMVC框架的消息转换器
WebMvcConfiguration.java相关代码编辑
有因为导包报错的 => 解决
自定义的对象转换器JacksonObjectMapper.java
添加员工功能
添加员工功能需求分析和设计
分析 => 查看产品原型
思考:新增员工相当于单表插入一条数据,应该对数据进行何种限制?
设计 ==> 接口文档
思考:
用何种请求方式? ==> post请求 ==> 前端传json给后端处理;
前端数据传输格式? ==> json
数据库如何设计员工表?
id字段 ==> 自增 (数据库自增ID、UUID(Universally Unique Identifier)、雪花算法)
用户名 ==> 唯一
状态 ==> 默认为1(正常为1,禁用为0)
设置EmployeeDTO封装前端传来的数据
为什么要用DTO封装而不直接使用实体类?
- 数据封装与解耦:避免直接暴露内部数据结构,后端数据变化而不影响前端。
- 安全性:可以去除不需要的字段,防止SQL注入攻击。
前端传输的数据与实体类的属性相差较大的时候,用DTO更安全。(@Data注解DAY1已提及)
@Data
public class EmployeeDTO implements Serializable {
private Long id;
private String username;
private String name;
private String phone;
private String sex;
private String idNumber;
}
属性字段与接口文档的body的值一一对应。
实现逻辑
EmployeeController:save()
- @PostMapping注解:定义接口路径
- 接收参数:接收json格式的DTO,要用@RequestBody注解
- 调用service的save方法(稍后定义),将DTO作为参数传入
- 返回值:封装为Result
@PostMapping注解:在EmployeeController类上已定义过,所以此处不必定义
(tips:自动生成注释 ==> 在方法上方输入/** + 回车)
EmployeeService & EmployeeServiceImpl :save()实现逻辑
- 过来的数据是DTO,但在持久层建议使用实体类 ==> 创建Employee实体
- 用BeanUtils.copyProperties(EmployeeDTO,实体) ==> 将属性名一致的完成拷贝
- 完善实体中EmployeeDTO缺失的属性
- 账号状态 ==> 默认正常状态
- 设置密码 ==> 默认密码
- 添加/修改时间 ==> 暂时硬写 (系统时间)
- 设置记录创建人 ==> 暂时硬写 (设置为10L)
EmployeeServiceImpl新增员工方法
状态常量类:
路径:src/main/java/com/sky/constant/StatusConstant.java
定义常量类目的:
EmployeeMapper
简单的sql语句用注解
如果在敲SQL语句时idea没有提示,参考如下方法设置SQL方言
代码开发部分完成(建议是跟着视频敲,本文的截图仅作补充,有些代码不全)
测试:
使用swagger测试接口
返回401状态码(未授权)
原因:请求被拦截器拦截,校验JWT令牌,未通过返回401
拦截器代码:
路径:src/main/java/com/sky/interceptor/JwtTokenAdminInterceptor.java
import com.sky.constant.JwtClaimsConstant; import com.sky.context.BaseContext; import com.sky.properties.JwtProperties; import com.sky.utils.JwtUtil; import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * jwt令牌校验的拦截器 */ @Component @Slf4j public class JwtTokenAdminInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; /** * 校验jwt * * @param request * @param response * @param handler * @return * @throws Exception */ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断当前拦截到的是Controller的方法还是其他资源 if (!(handler instanceof HandlerMethod)) { //当前拦截到的不是动态方法,直接放行 return true; } //1、从请求头中获取令牌 String token = request.getHeader(jwtProperties.getAdminTokenName()); //2、校验令牌 try { log.info("jwt校验:{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token); Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); log.info("当前员工id:", empId); BaseContext.setCurrentId(empId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; } } }
实现了HandlerInterceptor 接口,自定义拦截器,在MVC注册,实现拦截指定路径的请求
校验JWT步骤:
- 解析JWT:
- 将其分解为头部、载荷(Payload)和签名三部分。
- 验证头部:
- 确认 JWT 的头部(Header)部分是否符合预期,
- 通常包括算法(例如 HS256、RS256 等)和令牌类型(如 "JWT")等信息。
- 验证签名:
- 如果 JWT 使用了签名(如 HMAC 或 RSA 签名),需要使用相应的密钥来验证签名的有效性。
- 具体步骤包括:
- 使用头部中指定的算法解码签名部分。
- 将头部和载荷组合起来,并使用相同的算法和密钥重新计算签名。
- 将计算出的签名与 JWT 中的签名部分进行比较,确认它们是否匹配。
- 验证有效期:检查 JWT 中的有效期(exp)是否过期。
解决测试401问题:
在swagger设置全局参数
参数名称是啥?
根据连招可得,参数名为 token
参数值如何获取呢?
通过员工登录的返回值即可获取,复制粘贴确认即可
测试成功,新增员工代码初步完成!!!
完善代码
1)用户名重复 ==> 处理异常
原因:
- 需要完善的原因,如果用户名重复,按照上述逻辑处理,控制台会抛SQL异常,程序中止
处理:
- 定义全局异常GlobalExceptionHandler类
- 捕获SQL异常,当异常信息包含Duplicate entry字段时,认定为用户名重复异常
- 输出用户名重复信息
- 异常处理后不会终止程序。
代码:
思考:为什么不在异常前先判断用户是否存在?
做过多假定,假如新增用户数量多时,每次都先查再增,效率低。
2)动态设置创建记录人的id
思考:如何获取当前用户的id?
基于JWT令牌认证的流程
每次请求都会在请求头携带token
在生成token时用到了用户id
所以登录成功后,在后续的请求中,可以反向取出id,在拦截器解析token,获取id
思考:获取到id后,在拦截器我们并没有调用service,如何将id传给service操作?
这时请出我们的 Threadlocal 闪亮登场!!!
什么是Threadlocal ?常用方法?
Threadlocal 主要作用在同一线程内,那么
如何验证代码是否在同一线程执行?
在 拦截器、EmployeeController、EmployeeServiceImpl中输出Thread.currentThread.getId()查验
查看控制台发现一次请求输出的数值相同,每次请求的数值不同。
在开发中,使用Threadlocal 时往往封装为一个工具类
BaseContext.java类
路径:src/main/java/com/sky/context/BaseContext.java
为什么要封装为BaseContext?
简化代码:避免在多个类中代码冗余。
减少开销:该类都用static修饰符,在类加载时对象、方法就已经被初始化,且在整个生命周期中是单例的,所以ThreadLocal
实例不会重复创建,减少开销。
扩展性:对 ThreadLocal
的操作进行扩展或修改,可以集中在一个地方进行。例如,可以在 BaseContext
中添加日志记录、性能监控等功能,而不需要修改使用 ThreadLocal
的每一个地方。
新增员工功能实现、补充知识完结~
分页查询功能
需求分析与设计
分析 => 查看产品原型
思考:原型展示用到了什么参数?
分页查询常用的3个参数 :页码、每页展示数、(关键字)
设计 => 接口文档
思考:
用何种请求方式? ==> get请求 ==> 后端返回json数据展示;
前端数据传输格式? ==> query ==> 在路径上会有参数体现
返回参数:data{ total + 实体数组 } ==> 必须返回
设置EmployeePageQueryDTO封装前端传来的数据
实现逻辑
EmployeeController:page()
- @GetMapping注解:定义接口路径,补上类缺失的 " /page "
- 接收参数:接收query格式的DTO,不用@RequestBody注解
- 调用service的pageQuery方法(稍后定义),将DTO作为参数传入
- 返回值:封装的Result<PageResult>
PageResult类
records :当前页返回的 员工实体列表 (employeeList)
Result类
类声明:实现
Serializable
接口,使该类可以被序列化。序列化是将对象转换为字节流,以便保存到文件或通过网络传输。属性:状态码code、错误信息msg、数据data(泛型-传进来是List就是List)
3个重载的success方法:
无参:只返回状态码,一般用于post请求;
(T object):将传进来的对象赋值给data,返回result对象
(String msg):将传入的错误信息
msg
赋值给msg
属性,返回该实例。为什么要定义该类?
- 统一响应格式:使得所有 API 的响应结构/格式一致,便于前端处理。
- 支持泛型:通过泛型支持不同类型的数据,使这个类具有广泛的适用性。
- 便捷方法:提供静态方法方便地构建成功或失败的响应,减少重复代码。
/** * 后端统一返回结果 * @param <T> */ @Data public class Result<T> implements Serializable { private Integer code; //编码:1成功,0和其它数字为失败 private String msg; //错误信息 private T data; //数据 public static <T> Result<T> success() { Result<T> result = new Result<T>(); result.code = 1; return result; } public static <T> Result<T> success(T object) { Result<T> result = new Result<T>(); result.data = object; result.code = 1; return result; } public static <T> Result<T> error(String msg) { Result result = new Result(); result.msg = msg; result.code = 0; return result; } }
EmployeeService & EmployeeServiceImpl:pageQuery实现逻辑:
分页:将数据库中的数据按指定的 页码,每页展示数 计算出来的数据拼接
- 使用PageHelper插件,传入 页码、每页展示数 => 在每次查询SQL中拼接 limit...)
(使用Mybatis框架提供的PageHelper插件 ==> 简化代码)
- 调用mapper,根据DTO查询数据库,返回Page<实例>(使用PageHelper固定返回该对象)
- 如果DTO的name属性为空:查询整表;
- 如果DTO的name属性非空:根据name模糊查询;
- 将page封装为自定义的PageResult对象返回
使用PageHelper插件导:在pom文佳导入
EmployeeServiceImpl代码
PageResult.total:page.getTotal()
PageResult.records:page.getResult()
Page对象提供的方法:
返回一个long ==> 总数据数目
返回一个List<E> ==> 实体列表
EmployeeMapper
mapper.java代码
注意这个红色小鸟,点击跳转xml文件
因为该SQL语句动态且较为复杂,所以不用上述的注解方法,在xml文件中编写。
EmployeeMapper.xml代码:
- 如果DTO的name属性为空:查询整表;
- 如果DTO的name属性非空:根据name模糊查询;
- 根据创建时间排序
没有出现红、蓝小鸟如何解决?
点击setting => 安装插件MybatisX
功能测试
- 启动项目、ngnix、Mysql数据库
- 登录 => 配置全局参数 JWT令牌
- 使用swagger生成的接口文档测试
解决操作时间的格式问题:
解决方案
方案①:修改路径src/main/java/com/sky/entity/Employee.java
因为返回的是Page<Employee> ,所以对应在Employee.java中添加注解
如果使用这种方式,在每次返回时间数据时,都要对应加上该注解,代码冗余、不易维护。
方案②:扩展SpringMVC框架的消息转换器
WebMvcConfiguration.java相关代码
- 将自定义的转换器(Java对象 互相转换 json)设置为消息转换器
- 将这个消息装唤起注入MVC容器中,初始排最末尾,设置为0优先使用
有因为导包报错的 => 解决
该类用到的包:
自定义的对象转换器JacksonObjectMapper.java
路径:src/main/java/com/sky/json/JacksonObjectMapper.java