今日战报
继续完善用户相关接口开发:
1.完成获取用户信息功能
2.完成更新用户信息功能
3.完成更新用户头像功能
4.完成更新用户密码功能
获取用户信息
接口文档
如接口文档所示,我们需要做的就是从header中的Authorization中读取token,解码后返回用户的全部信息,接口如下:
@GetMapping("/userInfo")
public Result<User> userInfo(@RequestHeader(name = "Authorization") String token){
Map<String,Object> map = JwtUtil.parseToken(token);
String username = (String) map.get("username");
User user = userService.getByUserName(username);
return Result.success(user);
}
这是运行结果
这里会发现一个问题,我们返回的数据中有经过加密的用户密码,这显然是不合适的,所以我们需要解决这个问题,Spring也给我们提供了用于解决这个问题的注解
@JsonIgnore//SpringMVC把当前对象转换为json字符串时,忽略password,最终的json字符串中就没有password
把这行注解加到user实体类中的password变量声明的上方即可
还有一个小问题,我们在数据库中对于时间的名称使用的是_,例如创建时间create_time,而在实体类中使用的却是驼峰法,这样会导致mybatis从数据库中接收出的时间数据为null(上图是我解决了这个问题后截的),我么们需要在yml配置文件中解决这个问题
mybatis:
configuration:
map-underscore-to-camel-case: true #开启驼峰命名和下划线命名的自动转换
ThreadLocal
提供线程局部变量
用来存取数据:set()/get()
使用ThreadLocal存储的数据,线程安全
在ThreadLocal中,每个线程get到的数据只能是它自身set的,是读取不到其他线程set的数据的
而在TomCat运行时会给每个用户提供一个单独的线程,故可以通过ThreadLocal来在拦截器中set我们需要的信息,再去对应的接口中get信息,形成同一个线程内的数据共享,以减少参数的传递
ThreadLocal优化
先给出要用的ThreadLocal工具类
/**
* ThreadLocal 工具类
*/
@SuppressWarnings("all")
public class ThreadLocalUtil {
//提供ThreadLocal对象,
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();
//根据键获取值
public static <T> T get(){
return (T) THREAD_LOCAL.get();
}
//存储键值对
public static void set(Object value){
THREAD_LOCAL.set(value);
}
//清除ThreadLocal 防止内存泄漏
public static void remove(){
THREAD_LOCAL.remove();
}
}
然后在拦截器的preHandle方法中将方法内解析出的token数据通过get方法存储
try {
Map<String,Object> claims = JwtUtil.parseToken(token);
//将token解析出的树蕨存入到ThreadLocal中去
ThreadLocalUtil.set(claims);
//放行
return true;
然后在需要用到claims的接口中通过get()方法取出数据(以获取用户信息时为例)
@GetMapping("/userInfo")
public Result<User> userInfo(){
// Map<String,Object> map = JwtUtil.parseToken(token);
// String username = (String) map.get("username");
Map<String,Object> map = ThreadLocalUtil.get();
String username = (String) map.get("username");
User user = userService.getByUserName(username);
return Result.success(user);
}
当然,为了防止数据长期存储导致内存泄露,我们也需要在请求结束后释放掉存储的信息,在拦截器的 afterCompletion方法中调用方法类中的close方法即可
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//在请求结束后 清空ThreadLocal中的数据
ThreadLocalUtil.remove();
}
更新用户信息
接口文档
分析接口文档,我们需要从1json数据中获取一个User对象,然后更新其中的nickname和email,当然,也要写入新的更新时间。同时,参数校验也是保证程序健壮性不可或缺的一环
先看Controller层
@PutMapping("/update")
public Result update(@RequestBody User user){
userService.update(user);
return Result.success();
}
Service层
@Override
public void update(User user) {
user.setUpdateTime(LocalDateTime.now());
userMapper.update(user);
}
在Service层实现更新时间的写入
Mapper层
@Update("update user set nickname = #{nickname} , email = #{email} , update_time = #{updateTime} where id = #{id}")
void update(User user);
注意,等号前是数据库列名,#{}内是user实体类中的变量名
测试结果如上,当然,Header中要写入token(因为拦截器的存在)
参数校验
由于我们不提供username的修改,所以我们并不关注username的限制,我们要关注的参数校验主要集中在id与email中
我们可以使用如下注解
User实体类修改如下
@NotNull
private Integer id;//主键ID
@NotEmpty
@Pattern(regexp = "^//S{1,10}$")
private String nickname;//昵称
@NotEmpty
@Email
private String email;//邮箱
当然,为了使这些规则生效,在调用规则对应实体时我么要使用 @Validated注解来使规范生效
接口更新如下:
@PutMapping("/update")
public Result update(@RequestBody @Validated User user){
userService.update(user);
return Result.success();
}
更新用户头像
接口文档
@Override
public void updateAvatar(String avatarUrl) {
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");
userMapper.updateAvatar(avatarUrl,id);
}
读取文档,我们需要注意的点便是头像格式是一个url路径
请求方式也是之前没有用过的pATCH
Controller层
@PatchMapping("updateAvatar")
public Result updateAvatar(@RequestParam String avatarUrl){
userService.updateAvatar(avatarUrl);
return Result.success();
}
Service层
@Override
public void updateAvatar(String avatarUrl) {
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");
userMapper.updateAvatar(avatarUrl,id);
}
我们这里利用ThreadLocal来获取当前请求用户的id,一并传给mapper层
Mapper层
@Update("update user set user_pic = #{avatarUrl} , update_time = now() where id = #{id}")
void updateAvatar(String avatarUrl,Integer id);
我们通过sql自带的now()函数来获取更改的时间
参数校验
可以通过@URL来校验参数是否满足URL格式
Controller层更新如下
@PatchMapping("updateAvatar")
public Result updateAvatar(@RequestParam @URL String avatarUrl){
userService.updateAvatar(avatarUrl);
return Result.success();
}
更新用户密码
接口文档
阅读更新文档,json传递的数据不像以前可以直接对应到User的变量,所以我们需要使用一个map来接收数据
并且我们发现文档没有考虑密码可能会被修改为空,所以也需要校验一下密码格式
写的好累,直接上Controller层和dao层吧
@PatchMapping("updatePwd")
@Validated
public Result updatePwd(@RequestBody @Valid Map<String,String> params){
//校验参数数量
String oldPwd = params.get("old_pwd");
String newPwd = params.get("new_pwd");
String rePwd = params.get("re_pwd");
if (!StringUtils.hasLength(oldPwd) || !StringUtils.hasLength(newPwd) || !StringUtils.hasLength(rePwd))
{return Result.error("缺少必要参数");}
//校验原密码是否正确
Map<String,Object> map = ThreadLocalUtil.get();
String username = (String)map.get("username");
User lUser = userService.getByUserName(username);
if(!lUser.getPassword().equals(Md5Util.getMD5String(oldPwd)))
{return Result.error("原密码输入错误");}
//校验新密码是否符合格式
if(!newPwd.matches(PWD_REGEXP))
{return Result.error("新密码非法!");}
//校验newPwd与rePwd是否一致
if (!rePwd.equals(newPwd))
{return Result.error("两次填写新密码不一致");}
userService.updatePwd( newPwd);
return Result.success();
}
@Update("update user set password = #{newPwd} , update_time = now() where id = #{id}")
void updatePwd(String newPwd,Integer id);
对了,还用了一个正则,我单独放到正则类里去了(顺便把之前用到的正则全甩里面去了)
package com.cacb.pattern;
public class RegexPatterns {
public static final String PWD_REGEXP = "^\\S{11,16}$";
public static final String NICKNAME_REGEXP = "^\\S{1,10}$";
public static final String USERNAME_REGEXP = "^\\S{4,16}$";
}