Spring Boot实战-文章管理系统(1.用户相关接口)
一、用户相关接口
1.注解
-
@RestController:是一个组合注解,它结合了 @Controller 和 @ResponseBody 注解的功能(就相当于把两个注解组合在一起)。 在使用 @RestController 注解标记的类中,每个方法的返回值都会以 JSON 或 XML 的形式直接写入 HTTP 响应体中,相当于在每个方法上都添加了 @ResponseBody 注解。
-
@RequestMapping:表示共享映射,如果没有指定请求方式,将接收GET、POST、HEAD、OPTIONS、PUT、PATCH、DELETE、TRACE、CONNECT所有的HTTP请求方式。
-
@GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@PatchMapping 都是HTTP方法特有的快捷方式@RequestMapping的变体,分别对应具体的HTTP请求方式的映射注解
-
@Pattern:通常用于对字符串字段进行正则表达式(regex)匹配的约束。 这种注解通常和框架(例如 Spring、Hibernate Validator)一起使用,以确保输入的数据满足特定的格式(@Pattern(regexp = "^\\S{5,16}$") String username:约束用户名为5-16位非空字符)
-
正则表达式的含义如下:^:匹配字符串的开始位置; \\S匹配非空白字符(空白字符包括空格、制表符、换行符等); {5,16}表示前面的\\S(非空白字符)重复 5 到 16 次,也就是说输入的字符串必须由 5 到 16 个非空白字符组成; $:匹配字符串的结束位置。
-
-
@RequestBody和@RequestParam:
-
@RequestParam接收的参数是来自requestHeader中,通常用于GET请求,比如常见的url。
-
@RequestParam有三个配置参数:required是否必须传递参数,默认为true,必须; defaultValue可设置请求参数的默认值; value 为接收url的参数名(相当于key值)
-
@RequestParam用来处理 Content-Type 为 multipart/form-data;编码的内容,Content-Type默认为该属性
-
@RequestBody接收的参数是来自requestBody中,即请求体。一般用于处理非 Content-Type: application/form-data编码格式的数据,比如:application/json、application/xml、applicatioin/x-www-form-urlencoded等类型的数据
-
就application/json类型的数据而言,使用注解@RequestBody可以将body里面所有的json数据传到后端,后端再进行解析
-
@PutMapping("/update")
public Result update(@RequestBody @Validated User user){
userService.update(user);
return Result.success();
}
@PatchMapping("/updateAvatar")
public Result updateAvatar(@RequestParam @URL @NotEmpty String avatarUrl){
userService.updateAvatar(avatarUrl);
return Result.success();
}
根据文档要求:更新用户基本信息的请求参数为application/json格式,所以用@RequestBody标签
根据文档要求:更新用户头像基本信息的请求参数为queryStringn格式,所以用@RequestParam标签
-
@Data:lombok依赖下的注解,能够自动注入get,set等方法
-
@NoArgsConstructor:生成一个无参数的构造方法
-
@AllArgsConstructor:生成一个包含所有变量的构造方法,默认生成的方法是 public 的
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Student {
public String name;
public int age;
}
public class Student {
public String name;
public int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
-
@NotNull:非空标签,可以为 empty,不能为Null,一般用在 Integer 类型的基本数据类型的非空校验上
-
@NotEmpty:不能为 null,且长度必须大于 0,一般用在集合类上或者数组上
-
@NotBlank:只能作用在接收的 String 类型上,不能为 null,而且调用 trim() 后,长度必须大于 0,即必须有实际字符
-
上述参数在实体类中应用,然后在Controller层和@Validated标签搭配使用,能够限制参数输入类型
@Data
public class User {
@NotNull
private Integer id;//主键ID
private String username;//用户名
@JsonIgnore//让springMVC吧当前对象转换成json字符串的时候,忽略password,最终的json字符串就没有password这个属性了
private String password;//密码
@NotEmpty
@Pattern(regexp = "^\\S{1,10}$")
private String nickname;//昵称
@NotEmpty
@Email
private String email;//邮箱
private String userPic;//用户头像地址
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}
@PutMapping("/update")
public Result update(@RequestBody @Validated User user){
userService.update(user);
return Result.success();
}
-
@jsonIgnore:让springMVC吧当前对象转换成json字符串的时候,忽略注解对象,最终的json字符串就没有注解这个属性了
-
@RestControllerAdvice 与 @ExceptionHandler:两个标签结合使用时,它可以对所有 @RestController 控制器方法中的异常进行统一处理 。
-
@RestControllerAdvice注解用于类前
-
@ExceptionHandler注解用于标记方法,用于捕获指定类型的异常
-
@RestControllerAdvice
public class GlobalExceptionHandler {
// @RestControllerAdvice 与 @ExceptionHandler 结合使用时,它可以对所有 @RestController 控制器方法中的异常进行统一处理
// 该注解用于标记方法,用于捕获指定类型的异常。在这个例子中,Exception.class 表示处理所有类型的异常
@ExceptionHandler(Exception.class)
public Result handleException(Exception e){
e.printStackTrace();
// StringUtils.hasLength(e.getMessage()):这是 Spring 提供的 StringUtils 工具类中的方法,作用是判断异常消息 e.getMessage() 是否有内容(即消息长度是否大于 0)。
// 如果 e.getMessage() 不是空的或不为空白,则返回 true,否则返回 false
return Result.error(StringUtils.hasLength(e.getMessage())?e.getMessage():"操作失败");
}
}
2. 用户接口项目逻辑
2.1 项目执行过程整体逻辑
用户通过访问浏览器向服务器发送请求或从服务器接收请求;然后通过Controller层(controller层主要面向用户,用于实现相关代码逻辑);其次Controller层通过调用Service层,Service层主要用于实现面向Controller层的接口(多数Service接口都需要重写@Override);最后Service层通过连接Mapper层实现对数据库的操作;Mapper层是面向数据库的,在最底层实现对数据库的相关操作。
2.2 用户注册逻辑
2.2.1 UserController层逻辑
-
根据文档,确定传入的参数为username和password,通过@Pattern注解实现传入参数要求5~16位非空字符的要求
-
调用Userservice接口中的 findByUsername方法来判断当前注册的用户名是否存在,若存在,则返回用户名已经被占用;否则调用Userservice接口中的register方法来完成用户注册,在注册过程中使用post方法来让用户向服务器传递参数
@Autowired
private UserService userService;
@PostMapping("/register")
public Result register(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
//查询用户
User u = userService.findByUsername(username);
if (u == null) {
//没有占用
//注册
userService.register(username, password);
return Result.success();
} else {
//占用
return Result.error("用户名已被占用");
}
}
2.2.2 UserService层逻辑
- 首先定义Userservice接口,findByUsername方法中,通过username来进行查询;register方法中传递username和password来完成用户注册(Userservice接口只是一个方法的定义,具体实现方法要重写)
- 重写Userservice接口:调用UserMapper接口中的findByUsername方法和add方法,来完成对数据库的操作
在存储password时,为了保证安全性,不能直接将用户的密码显性的存储在数据库中,通过使用加密工具Md5Util来完成对用户密码的加密(该加密在Userservice层中实现,因为Controller层面向用户,用于实现登录和注册的主题逻辑,不宜用于加密,Mapper层面向数据库,若是传入明文在加密,安全性不高,Userservice层在两者中间,既能够保证面相用户的逻辑不被破坏,又可以保证一定的安全性)
上述内容纯纯个人理解!!!小白一个!!!
public interface UserService {
User findByUsername(String username);
void register(String username, String password);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User findByUsername(String username) {
User user = userMapper.findByUsername(username);
return user;
}
@Override
public void register(String username, String password) {
//加密
String md5String = Md5Util.getMD5String(password);
userMapper.add(username,md5String);
}
}
2.2.3 UserMapper层逻辑
- 分别使用@Select和@Insert注解和数据库的相关方法来完成对数据库的操作
@Mapper
public interface UserMapper {
@Select("select * from user where username=#{username}")
User findByUsername(String username);
@Insert("insert into user(username,password,create_time,update_time)" +
" values(#{username},#{password},now(),now())")
void add(String username, String password);
}
2.3 用户登录逻辑
2.3.1 UserController层逻辑
- 通过@Pattern注解来判断用户输入的username和password是否符合要求
- 调用Userservice接口中的 findByUsername方法来判断当前注册的用户名是否存在,若不存在,则显示用户名错误提示
- 调用Md5Util类中的.getMD5String方法来将用户在浏览器输入的密码进行加密,然后用.equals方法来将用户在浏览器端输入的密码与数据库中存储的密码进行比对(因为user对象中存储的password是密文,所以只能密文比较)
- 若是用户名和密码都正确,则要给登录成功的用户分发一个JWT令牌,然后在后续的每次请求中,浏览器都需要在请求头header中携带到服务端,请求头的名称为 Authorization,值为登录时下发的JWT令牌。
@PostMapping("/login")
public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
//根据用户名查询用户
User loginUser = userService.findByUsername(username);
//判断该用户是否存在
if (loginUser == null) {
return Result.error("用户名错误");
}
//判断密码是否正确 loginUser对象中的password是密文
if (Md5Util.getMD5String(password).equals(loginUser.getPassword())) {
//登录成功
//创建HashMap用来存储载荷
Map<String,Object> claims = new HashMap<>();
//Hashmap中的value值为id,username
claims.put("id",loginUser.getId());
claims.put("username",loginUser.getUsername());
//生成令牌
String token = JwtUtil.genToken(claims);
return Result.success(token);
}
return Result.error("密码错误");
}
2.3.1.2 JWT令牌
package com.liumaji.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
private static final String KEY = "Liumaji";
//接收业务数据,生成token并返回
public static String genToken(Map<String, Object> claims) {
//创建JWT令牌
return JWT.create()
//添加令牌载荷,在这里用的是HashMap存储载荷(KEY:claims,value:Object集合)
.withClaim("claims", claims)
//令牌过期时间
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))
//带有秘钥的加密算法
.sign(Algorithm.HMAC256(KEY));
}
//接收token,验证token,并返回业务数据
public static Map<String, Object> parseToken(String token) {
//加密算法
return JWT.require(Algorithm.HMAC256(KEY))
//生成验证器
.build()
//验证token并解析
.verify(token)
//获取key名为claims的value值
.getClaim("claims")
//以hashmap形式存储解析出来的值
.asMap();
}
}
JWT令牌:
2.3.2 UserService层逻辑
- 同注册逻辑
2.3.3 UserMapper层逻辑
- 同注册逻辑
2.3.4 拦截器
为了防止用户没有登录,而是直接通过输入网址来进行信息查询和更改,所以要引入拦截器以保证只有进行过登录的用户才能进行相应操作。
- @Component:将拦截器对象注册到IOC容器里面
因为用户的所有请求数据都在HttpServletRequest对象里面,所以在拦截器对象中定义的方法里面传入HttpServletRequest参数,来保证所有的用户请求都可以被拦截(登录和注册请求除外)
- 首先根据用户请求参数里面名为header(即Authorization)的部分来获取令牌
- 其次调用JwtUtil类中的解析办法parsetoken进行解析,若是能够解析,则说明用户已经登录;将成功解析的用户数据放到一个线程池(ThreadLocal中)并放行,否则就是用户未登录,则浏览器报401错误。
成功解析令牌的用户返回应该是JWT有效载荷部分(id,username等)
成功解析的令牌例子:
package com.liumaji.interceptors;
import com.liumaji.dto.Result;
import com.liumaji.utils.JwtUtil;
import com.liumaji.utils.ThreadLocalUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Map;
//拦截器的逻辑代码
@Component//拦截器对象注册到IOC容器里面
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception{
//令牌验证
//所有请求数据都在Request对象里面
String token = request.getHeader("Authorization");
try {
Map<String, Object> claims = JwtUtil.parseToken(token);
//把业务数据存储到ThreadLocal中
ThreadLocalUtil.set(claims);
return true;//放行
} catch (Exception e) {
//http 响应状态码401
response.setStatus(401);
return false;//不放行
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清空ThreadLocal中的数据
ThreadLocalUtil.remove();
}
}
/注册拦截器
//配置类(WebConfig)也需要注册到IOC容器里面
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry){
//登录接口和注册接口不拦截
registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login","/user/register");
}
}
2.3.5 线程池
ThreadLocal是为了让每个用户只能使用自己部分的数据,防止与其他用户数据共享发生冲突,ThreadLocal类中每个方法的用处已在代码中注释。
package com.liumaji.utils;
import java.util.HashMap;
import java.util.Map;
/**
* ThreadLocal 工具类
*/
@SuppressWarnings("all")
public class ThreadLocalUtil {
//提供ThreadLocal对象,维护全局唯一一个THREAD_LOCAL
//private static final 一起使用:当这三个关键字一起使用时,表示该成员是类级别的常量,并且只能在类内部访问
//private:该常量只能在 ThreadLocalUtil 类内部访问。
// static:该常量属于类本身,而不是某个实例。所有实例共享同一个值,并且可以在没有实例的情况下访问。
//final:该常量一旦被赋值后就不能再修改
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();
//根据键获取值
//采用泛型方法get,因为对于每个用户的THREAD_LOCAL可能存放多种数据类型,所以采用泛型方法以适配各种数据类型
//在取出相应数据后进行相应的数据转换
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();
}
}
2.4 获取用户详细信息逻辑
2.4.1 UserController层逻辑
- 从线程池ThreadLocal中获取key为(claims)的用户相关信息(因为只要成功登录的用户,他的相关信息都放在了线程池中(在拦截器中实习))
- 获取登录的用户的username
- 根据用户名返回用户的相关信息
@GetMapping("/userInfo")
public Result<User> userInfo(/*@RequestHeader(name = "Authorization") String token*/){
//根据用户名查询用户
// 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.findByUsername(username);
return Result.success(user);
}
2.4.2 UserService层逻辑
- 同登录逻辑
2.4.3 UserMapper层逻辑
-
同登录逻辑
2.5 更新用户基本信息逻辑
2.5.1 UserController层逻辑
- 根据接口文档可知,用户请求的数据格式为application/json格式,所以参数选择用@RequestBody注解封装User类信息
- 调用service层的update方法,完成用户信息更新
- 为满足接口文档中的参数要求:
- 在实体类中用相应标签进行限制
- 在Controller层搭配使用@Validated注解来完成对实体类User中相关参数的限制
@PutMapping("/update")
public Result update(@RequestBody @Validated User user){
userService.update(user);
return Result.success();
}
@Data
public class User {
@NotNull
private Integer id;//主键ID
private String username;//用户名
@JsonIgnore//让springMVC吧当前对象转换成json字符串的时候,忽略password,最终的json字符串就没有password这个属性了
private String password;//密码
@NotEmpty
@Pattern(regexp = "^\\S{1,10}$")
private String nickname;//昵称
@NotEmpty
@Email
private String email;//邮箱
private String userPic;//用户头像地址
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}
2.5.2 UserService层逻辑
- 在service接口中定义update方法
- 重写接口中的update方法,调用Mapper层的update方法,完成对数据库的更新操作
@Override
public void update(User user) {
user.setUpdateTime(LocalDateTime.now());
userMapper.update(user);
}
2.5.3 UserMapper层逻辑
- 在这里updatetime是接收从service接口中传递过来的时间进行更新,也可以使用数据库函数now()来完成更新时间的更新(同注册Mapper)
@Update("update user set nickname=#{nickname},email=#{email},update_time=#{updateTime} where id = #{id}")
void update(User user);
2.6 更新用户头像逻辑
2.6.1 UserController层逻辑
- 根据接口文档,用户请求数据格式为url,所以使用@RequestParam注解来封装avtaUrl,同时,因为它的数据格式得保证为url形式,所以加了一个@URL注解
- 该url存放的是图片所在的url
然后本人在实验过程中发现当传递空的url时,也可以更新头像,所以为了防止这种情况,自己加入了@NotEmpty注解。
在刚开始时,想想更新用户基本信息一样,在User实体类中用户头像地址参数中加入@URL然后在Controller层和@Validated注解搭配使用,从而实现参数为URL地址的限制,但是没有实现。
原因说明:因为是在User实体类里面限制的用户头像地址参数,但在Controller层里面更新用户头像的请求参数并不是User实体类,而搭配@Validated注解只能限制将User实体类作为请求参数的限制,所以@URL使用在User实体类并不能限制
@PatchMapping("/updateAvatar")
public Result updateAvatar(@RequestParam @URL @NotEmpty String avatarUrl){
userService.updateAvatar(avatarUrl);
return Result.success();
}
2.6.2 UserService层逻辑
- 在service层首先调用ThreadLocalUtil.get(),获取用户的相关信息
- 然后获取用户的相关id(为什么在更新用户信息不用获取id呢?因为在controller层传递的参数为user对象,里面有id,username等唯一性信息,所以不会定位错;而在更新头像时,只是传递了url在controller层,无法确定要更新哪个用户的头像,所以在service层需要获取用户id,以调用mapper层的方法来完成对数据库的更新)
- 调用mapper层的相关方法,完成对数据库的更新
void updateAvatar(String avatarUrl);
@Override
public void updateAvatar(String avatarUrl) {
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");
userMapper.updateAvatar(avatarUrl,id);
}
2.6.3 UserMapper层逻辑
@Update("update user set user_pic = #{avatarUrl},update_time=now() where id = #{id}")
void updateAvatar(String avatarUrl,Integer id);
2.7 更新用户密码逻辑
2.7.1 UserController层逻辑
- 根据接口文档:请求参数类型为json类型,然后是HashMap<String,String>类型
- 首先获取请求参数的相关信息
- 其次判断三个请求参数是否都有,若有空的,则返回错误
- 因为能够更新用户密码的前提是已经登录,所以从线程池中获取用户的相关信息
- 然后获取用户的username,然后根据username获取用户User实体类的相关参数
- 调用User实体类中的loginUser.getPassword()方法获取其在数据库中已经加密了的密码,和用户已经加密的原密码进行比较
- 最后在比较两次输入的新密码是否一致
- 若上述全部通过,则调用service层的更新密码的方法
@PatchMapping("/updatePwd")
public Result updatePwd(@RequestBody 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("缺少必要的参数");
//校验原密码是否正确
//调用Userservice拿到用户原密码,再和old_pwd比对
Map<String,Object> map = ThreadLocalUtil.get();
String username = (String) map.get("username");
User loginUser = userService.findByUsername(username);
if(!loginUser.getPassword().equals(Md5Util.getMD5String(oldPwd))){
return Result.error("原密码填写不正确");
}
if (!rePwd.equals(newPwd)){
return Result.error("两次填写的新密码不一样");
}
//调用sevice完成密码更新
userService.updatePwd(newPwd);
return Result.success();
}
2.7.2 UserService层逻辑
-
使用ThreadLocal获取用户的相关信息
-
获取用户的id
- 根据用户的id将经过加密的新密码更新在数据库
@Override
public void updatePwd(String newPwd) {
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");
userMapper.updatePwd(Md5Util.getMD5String(newPwd),id);
2.7.3 UserMapper层逻辑
@Update("update user set password = #{md5String},update_time=now() where id = #{id}")
void updatePwd(String md5String, Integer id);