Springboot学习笔记

基础篇

配置文件

采用.yml格式的配置文件相较于.property格式的配置文件来说,层级关系更清晰

image-20240405132413536

yml配置信息的书写与获取

  • 三方技术配置信息
    • Mybatis
  • 自定义配置信息
    • 邮件的相关配置信息

使用注解进行值的获取

1
@ConfigurationProperties(prefix = "email")

注意:

  • 值的前面必须有空格作为分隔符
  • 使用空格作为缩进表示层级关系,相同层级左侧对齐

配置信息的获取

1
2
3
4
// 获取值
@Value("${键名}")
// 前缀
@ConfigurationProperties(prefix = "email")

Springboot整合mybatis

Bean注册

image-20240405210815362

如果要注册的bean对象来自于第三方(不是自定义的),是无法使用@Component及其衍生注解声明bean的

  • @Bean
  • @Import

Bean扫描

Springboot默认扫描启动类所在包及其子包下的内容,如果要扫描其他文件夹,可以手动添加注解@ComponentScan

image-20240406165916427

注册条件

image-20240408201847651

自动配置原理

遵循约定大于配置的原则,在boot程序启动后,起步依赖中的一些bean对象会自动注入到ioc容器中

实战篇

环境搭建

image-20240409171023782

准备工作:

  • 创建数据库表
  • 创建SpringBoot工程,引入对应的依赖(web、mybatis、mysql驱动)
  • 配置文件application.yml中引入mybatis的配置信息
  • 创建包结构,并准备实体类

包结构介绍如下:

  • config:对应相关的配置信息
  • controller:控制器存放的包
  • exception:全局异常
  • interceptors:拦截器
  • mapper:和数据库相关的
  • pojo:实体类相关的
  • service:接口和对应的实现类,核心功能都由其实现
  • utils:存放工具类,工具类中通常会提供静态方法供我们在项目中使用

image-20240412092156313

功能:

  • 注册
  • 登录
  • 获取用户详细信息
  • 更新用户基本信息
  • 更新用户头像
  • 更新用户密码

故障修复

image-20240409202558370

对于下面这个报错,只需要将mybatis的版本修改为3.0.3即可解决

Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: No acceptable representation]

这个报错是因为没有在标准结果Result对象上面添加@Data注解,就导致解析的时候无法正确的将其转换为json对象。

image-20240412095712214

上面这个报错解决方案是在userMapper中的文件上添加注解

1
2
@Insert("insert into user(username,password,create_time,update_time) values(#{username},#{password},now(),now())")
void add(@Param("username") String username, @Param("password") String password);

用户模块

注册

注册相关的api要求

定义统一的消息返回的类Result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package org.itheima.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private Integer code;//业务状态码 0-成功 1-失败
private String message;//提示信息
private T data;//响应数据

//快速返回操作成功响应结果(带响应数据)
public static <E> Result<E> success(E data) {
return new Result<>(0, "操作成功", data);
}

//快速返回操作成功响应结果
public static Result success() {
return new Result(0, "操作成功", null);
}

public static Result error(String message) {
return new Result(1, message, null);
}
}

其中我们定义了三个静态方法:

  • success(E data)
  • success():操作成功方法,无需传入相关参数
  • error(String message)

注意这里使用了泛型,这样我们data类型就可以根据实际情况进行灵活的调整与使用了

注册接口

步骤:

  1. 编写UserController类
  2. 编写UserService接口
  3. 编写UserServiceImpl实现UserService接口
  4. 编写注册逻辑register方法

详细代码如下:

UserController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package org.itheima.controller;

import jakarta.validation.constraints.Pattern;
import org.itheima.pojo.Result;
import org.itheima.pojo.User;
import org.itheima.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;

@PostMapping("/register")
public Result register(String username, String password) {
// 查询用户
User u = userService.findByUserName(username);
if (u == null) {
// 用户名没有被占用
// 注册
userService.register(username, password);
return Result.success("注册成功");
} else {
// 占用
return Result.error("用户名已被占用");
}
// 注册
}
}

UserService接口

1
2
3
4
5
6
7
8
9
10
11
package org.itheima.service;

import org.itheima.pojo.User;
import org.springframework.stereotype.Service;

@Service
public interface UserService {
User findByUserName(String username);

void register(String username, String password);
}

编写UserServiceImpl实现UserService接口

这里的实现中引入了一个密码加密处理的工具类MD5Util,实际上MD5加密的方式是不安全的,可以通过撞库的方式进行密码的破解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package org.itheima.service.impl;

import org.itheima.mapper.UserMapper;
import org.itheima.pojo.User;
import org.itheima.service.UserService;
import org.itheima.utils.Md5Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;

@Override
public User findByUserName(String username) {
User u = userMapper.findByUserName(username);
return u;
}

@Override
public void register(String username, String password) {
// 密码加密处理
String md5String = Md5Util.getMD5String(password);
// md5 加密
userMapper.add(username, md5String);
}
}

UserMapper类中注册方法:注意add方法需要添加注解@Param(“username”)和@Param(“password”),否则会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.gyz.demoregister.mapper;

import com.gyz.demoregister.pojo.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

@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(@Param("username") String username, @Param("password") String password);
// void add(String username, String password);
}

测试

image-20240412100155489

注册校验

在上面的代码中,存在一定的缺陷,即用户名和密码需要进行一定的校验才能符合我们的要求,如下图所示密码要求是5-16位非空字符,在我们刚刚的代码中没有进行相关的校验,不符合要求,现在我们来进行改进。

image-20240412100450075

这里我们使用Spring Validation ,这是Spring提供的一个参数校验框架,使用预定义的注解完成参数校验

步骤主要有三步:

  • 引入Spring Validation起步依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • 在参数前面添加@Pattern注解

1
public Result register(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
  • 在Controller类上添加@Validated注解

在控制器类上方添加@Validated注解

1
2
@Validated
public class UserController {

image-20240412101543115

这时候我们进行测试:会发现报错500服务器内部错误,这样的响应格式明显是不符合我们要求统一的返回类型Result

image-20240412102352488

这时候我们就可以定义一个全局异常处理器,用来处理参数校验失败的异常

使用注解@RestControllerAdvice,方法上面需要添加注解@ExceptionHandler(Exception.class),返回值类型为Result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.gyz.demoregister.exception;

import com.gyz.demoregister.pojo.Result;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result handlerException(Exception e){
e.printStackTrace();
return Result.error(StringUtils.hasLength(e.getMessage())? e.getMessage():"操作失败");
}
}

这里使用spring提供的StringUtils工具类判断错误信息是否为空,如果不为空则返回错误信息,如果为空则返回操作失败。

登录

请求路径 /user/login

请求方式 POST

请求参数username和password

响应数据:Result对象,其中data返回JWT

用户登录成功后,系统会自动下发JWT令牌,然后后续每次请求,都会在请求头header中携带Authorization,值为JWT令牌

如果检测到用户未登录或者JWT过期或非法,则HTTP响应码为401

登录逻辑

  • 首先判断用户名是否存在于数据库中
  • 如果存在则进行密码校验
    • 如果密码错误则返回账号名或密码错误
    • 如果密码正确则返回JWT令牌
  • 如果不存在则返回账号名或密码错误

注意claims为存储到jwt中的数据,同时这个数据后续也会存在ThreadLocal中方便在其他的DAO接口中进行复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@PostMapping("/login")
public Result login(String username, String password) {
User loginUser = userService.findUserByName(username);
if (loginUser == null) {
return Result.error("用户名或密码错误!");
}
if (Md5Util.getMD5String(password).equals(loginUser.getPassword())) {
Map<String, Object> claims = new HashMap<>();
// 存储到ThreadLocal中的业务数据 这里跟需要来即可
claims.put("id", loginUser.getId());
claims.put("username", loginUser.getUsername());
String token = JwtUtil.genToken(claims);
return Result.success(token);
}

return Result.error("用户名或密码错误!");
}

登录认证引入

例如我们登录过后,如果访问文章列表这样的信息的话,就需要携带我们登录过后的令牌才能够进行访问否则直接拒绝访问。例如下面的接口在我们没有登录的状态下是不能直接访问的。

image-20240412152258937

令牌就是一段字符串

  • 承载业务数据,减少后续请求查询数据库的次数【直接从令牌中获取用户的信息】
  • 防篡改,保证信息的合法性和有效性

JWT

.分成三个部分

  • Header(请求头),记录令牌类型、签名算法等。{“alg” : “H256”, “type” : “JWT”}
  • Payload(有效载荷),携带一些自定义信息、默认信息等。例如{“id”: “1”, “username” : “zhangsan”}
    • 此部分不要存放一些加密信息
  • Signature(签名),防止Token被篡改、确保安全性,将header、payload进行加密得到

jwt基于Base64进行编码,这样就算数据中个包含了中文也是可以正确显示的

生成jwt

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE3MTI3MzM5OTh9.6cBZ03xMeLItFt4FWSM_Xe2lBcGklI8koRpZZINa-Zk

校验jwt token

如果篡改了头部和载荷部分的数据,那么验证失败

如果秘钥修改了,验证失败

token过期

image-20240410153251962

使用jwt

引入依赖坐标,刷新Maven

1
2
3
4
5
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>

创建测试类,生成jwt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.gyz.cvmanage;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.junit.jupiter.api.Test;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtTest {
// jwt生成
@Test
public void testGen() {
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("username", "张三");

String token = JWT.create()
.withClaim("user", claims)
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12)) // 添加过期时间
.sign(Algorithm.HMAC256("itheima"));// 指定算法,配置密钥
System.out.println(token);
}

// jwt解析
@Test
public void testParse() {
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE3MTI5NTA3MTR9.2C1Ljtu7VJjjOyw5_vS8j0GFMxVXhPVmzhswSdUjxDo";
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("itheima")).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
Map<String, Claim> claims = decodedJWT.getClaims();
System.out.println(claims); // {exp=1712950714, user={"id":1,"username":"张三"}}
}

}
1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE3MTI5NTA3MTR9.2C1Ljtu7VJjjOyw5_vS8j0GFMxVXhPVmzhswSdUjxDo

解密jwt

1
{exp=1712950714, user={"id":1,"username":"张三"}}

登录成功返回jwt

image-20240412160125393

现在进行校验:即访问**/article/list**接口需要携带token请求头Authorization才能获取对应的数据。

从请求头中获取数据可以使用注解@RequestHeader

1
Map<String, Object> claims = JwtUtil.parseToken(token);

这段解析的代码如果报错则说明解析失败,如果没有报错则解析成功,可以从其中获取我们所需要的数据

Ctrl+Alt+T idea快捷键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/article")
public class ArticleController {
@GetMapping("/list")
public Result<String> list(@RequestHeader(name = "Authorization") String token, HttpServletResponse response) {
// 验证token
try {
Map<String, Object> claims = JwtUtil.parseToken(token);
return Result.success("所有文章数据");
} catch (Exception e) {
// throw new RuntimeException(e);
response.setStatus(401); // 未授权 校验失败
return Result.error("未登录.....");
}
}
}

测试如下:

image-20240412162032583

上面的代码的确实现了我们所需要的功能,但是存在一个缺陷,如果我们增加业务代码,就需要在代码中编写token校验的内容,这样的工作显然是比较麻烦的,后续为了方便所有的接口都进行jwt的校验,可以编写一个拦截器来进行拦截,如下图所示:

image-20240410155952755

image-20240410160025628

拦截器的实现步骤:

  • 编写LoginInterceptor类实现HandlerInterceptor接口
    • 重写preHandle方法:在请求到达处理器之前,可以用于权限验证、数据校验等操作。如果返回true,表示方放行继续执行后续代码;如果返回false,表示拦截,中断请求处理。
  • 编写web配置类实现WebMvcConfigurer接口,注册我们写好的拦截器

拦截器LoginInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 令牌验证
String token = request.getHeader("Authorization");
try {
Map<String, Object> claims = JwtUtil.parseToken(token);
return true; // 放行
} catch (Exception e) {
// throw new RuntimeException(e);
response.setStatus(401); // 未授权 校验失败
return false; // 拦截
}
// return HandlerInterceptor.super.preHandle(request, response, handler);
}
}

注册拦截器:这里需要注意添加注解@Configuration,否则配置不会生效

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor).excludePathPatterns(
"/user/login", // 放行登录接口
"user/register"// 放行注册接口
);
}
}

获取用户详细信息

根据用户名获取用户详细信息,用户名从token中获取:

  1. 从请求头中获取token
  2. 调用jwt工具类对token进行解析
  3. 根据用户名查找用户
  4. 返回用户实例
1
2
3
4
5
6
7
8
@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.findUserByName(username);
return Result.success(user);
}

测试1:不携带Authorization请求头,HTTP响应码为401未授权

image-20240412171339061

测试2:携带Authorization请求头,得到结果:

image-20240412171423456

这里需要注意:接口响应的数据中包含了密码,这显然是不符合要求的【密码属于机密信息需要进行隐藏】。Springboot提供了对应的注解@JsonIgnore

让springMVC把当前对象转换为json字符串的时候,忽略password,最终的json字符串就没有password这个属性了

1
2
3
4
5
6
7
8
9
10
11
12
@Data
public class User {
private Integer id;// 主键ID
private String username;// 用户名
@JsonIgnore
private String password;// 密码
private String nickname;// 昵称
private String email;// 邮箱
private String userPic;// 用户头像地址
private LocalDateTime createTime;// 创建时间
private LocalDateTime updateTime;// 更新时间
}

重启项目,再次测试,得到正确的结果:

image-20240412173229726

但是我们可以发现,返回的结果中创建时间和更新时间是null,但数据库中实际上是存在这个数据的,这是因为数据库中的字段是带下划线的,无法和驼峰命名进行自动转化,我们需要在application.yml中添加相应的配置信息【来进行驼峰和下划线的自动转换】

image-20240412173400545

1
2
3
mybatis:
configuration:
map-underscore-to-camel-case: true

重新测试,结果正确:

测试创建和更新时间

ThreadLocal优化

上面的代码中我们为了获取用户的信息,在userInfo接口中解析了jwt的token,但是实际上我们在拦截器中已经进行解析了;相当于这部分我们进行了重复的工作,因此我们需要对其进行优化,解决的方法就是使用ThreadLocal存储用户的信息

ThreadLocal提供线程的局部变量

  • 用来存取数据:get()/set()
  • 使用ThreadLocal存储的数据,线程安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ThreadLocalTest {
@Test
public void testThreadLocalSetAndGet() {
// 提供ThreadLocal对象
ThreadLocal tl = new ThreadLocal();

new Thread(() -> {
tl.set("11111111111111");
System.out.println(Thread.currentThread().getName() + " : " + tl.get());
System.out.println(Thread.currentThread().getName() + " : " + tl.get());
System.out.println(Thread.currentThread().getName() + " : " + tl.get());
}, "蓝色线程").start();
new Thread(() -> {
tl.set("22222222222222");
System.out.println(Thread.currentThread().getName() + " : " + tl.get());
System.out.println(Thread.currentThread().getName() + " : " + tl.get());
System.out.println(Thread.currentThread().getName() + " : " + tl.get());
}, "红色线程").start();

}
}

上面的代码中,创建了两个线程并在线程中分别存放了不同的数据,最终运行测试程序,发现最终两个线程存储的内容是相互隔离的。

ThreadLocal原理如下:两个用户的线程中的数据是相互隔离的,所以是线程安全的。

image-20240410201706484

使用ThreadLocal对代码进行优化步骤十分简单:

  • 引入ThreadLocalUtil工具类
  • 在拦截器中添加数据到ThreadLocal中
  • 在对应的业务代码中获取ThreadLocal中的数据
  • afterCompletion方法中清空ThreadLocal对象防止内存泄漏

拦截器中存储数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 令牌验证
String token = request.getHeader("Authorization");
try {
Map<String, Object> claims = JwtUtil.parseToken(token);
// System.out.println(claims);
// 将业务信息存储到ThreadLocal中
ThreadLocalUtil.set(claims); // {id=5, username=test2}
return true; // 放行
} catch (Exception e) {
// throw new RuntimeException(e);
response.setStatus(401); // 未授权 校验失败
return false; // 拦截
}
// return HandlerInterceptor.super.preHandle(request, response, handler);
}

userInfo接口

直接从全局的ThreadLocal对象中获取数据即可

1
2
3
4
5
6
7
8
9
10
11
@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.findUserByName(username);
return Result.success(user);
}

更新用户基本信息

image-20240412202722804

更新参数为nickname、email,需要根据id来进行更新,更新的时候需要注意更新时间

restful风格规定了我们进行更新的时候使用的方法为PUT

步骤如下:

image-20240412203112871

完成上述工作后,仍然存在一个问题:参数的校验,这里我们更新的昵称的长度以及email的格式需要符合规范,所以这里需要进行参数的校验;但是由于这些参数都是在User实体类中的,所以需要借助于自带的注解来进行参数的校验

参数校验

更新信息参数校验 【实体参数校验】

  1. 需要在实体类的属性上面添加validation提供的注解
  2. 在参请求参数前面添加@Validated注解

image-20240410210412049

实体类属性上添加相应注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
public class User {
@NotNull // 值不为null
private Integer id;// 主键ID
private String username;// 用户名
@JsonIgnore
private String password;// 密码
@NotEmpty // 不能为null并且内容不为空
@Pattern(regexp = "^\\S{1,10}$")
private String nickname;// 昵称
@NotEmpty
@Email // 满足邮箱格式
private String email;// 邮箱
private String userPic;// 用户头像地址
private LocalDateTime createTime;// 创建时间
private LocalDateTime updateTime;// 更新时间
}

controller中添加注解 @Validated 注解

1
2
3
4
5
@PutMapping("/update")
public Result update(@RequestBody @Validated User user) {
userService.update(user);
return Result.success();
}

测试更新成功:

image-20240412204858410

nickname不能为空

image-20240412204927627

邮箱地址不合法无法通过

image-20240412204959267

更新用户头像

请求路径:/user/updateAvatar

PATCH:这个更新通常用于部分更新,比如本需求中只需要更新图片

这里也需要进行参数校验,可以使用@url进行校验

1
2
3
4
5
@PatchMapping("/updateAvatar")
public Result updateAvatar(@RequestParam @URL String avatarUrl){
userService.updateAvatar(avatarUrl);
return Result.success();
}

image-20240412210003005

更新头像

image-20240410212847940

这个报错是因为参数里面并没有这个值,需要自己手动进行获取,这里使用now()方法进行获取即可

更新成功

image-20240410212958895

@URL注解可以自动对url地址进行校验

image-20240412210514448

更新用户密码

请求接口 /user/updatePwd

PATCH:本需求中只需要更新用户密码

请求参数包括三个:

  • old_pwd
  • new_pwd
  • re_pwd

image-20240412211049990

注意这里在进行密码更新的时候,id需要在ThreadLocal中进行获取

测试成功,并对错误接口进行测试

image-20240412212759929

image-20240412213026492

image-20240412213037139

文章分类

总共有5个接口:

  • 新增文章分类
  • 文章分类列表
  • 获取文章分类详情
  • 更新文章分类
  • 删除文章分类

新增文章分类

请求路径:/category

请求方式:POST

接口描述:该接口用于新增文章分类

请求数据样例

1
2
3
4
{
"categoryName":"人文",
"categoryAlias":"rw"
}

数据库表结构如下:

image-20240413165627307

  • id

  • 分类名称

  • 分类别名

  • 创建人ID,来自于User表中的主键ID,记录这个分类是哪个用户进行创建的

  • 创建时间

  • 更新时间

1
2
3
4
5
6
7
8
public class Category {
private Integer id;// 主键ID
private String categoryName;// 分类名称
private String categoryAlias;// 分类别名
private Integer createUser;// 创建人ID
private LocalDateTime createTime;// 创建时间
private LocalDateTime updateTime;// 更新时间
}

image-20240413170319757

文章分类列表

请求参数:无

请求方式:GET

请求路径:/category

注意这个请求路径和新增文章分类是一样的,不同的是他们的请求方式不一样。

因为这里返回的数据是如下格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"code": 0,
"message": "操作成功",
"data": [
{
"id": 3,
"categoryName": "美食",
"categoryAlias": "my",
"createTime": "2023-09-02 12:06:59",
"updateTime": "2023-09-02 12:06:59"
},
{
"id": 4,
"categoryName": "娱乐",
"categoryAlias": "yl",
"createTime": "2023-09-02 12:08:16",
"updateTime": "2023-09-02 12:08:16"
},
{
"id": 5,
"categoryName": "军事",
"categoryAlias": "js",
"createTime": "2023-09-02 12:08:33",
"updateTime": "2023-09-02 12:08:33"
}
]
}

经过观察可以发现这也就是Category实体类所对应的数据,所以我们在控制器中写我们方法的时候返回值需要使用Result的泛型:

1
2
3
4
5
@GetMapping
public Result<List<Category>> list() {
List<Category> cs = categoryService.list();
return Result.success(cs);
}

这里通过categoryService中的list方法获取到一个List集合得到全部的数据。【注意:这里所得到的数据需要是当前用户所创建的数据所以在list方法中还需要从ThreadLocal中获取到用户id,然后在userMapper中查询的时候再根据用户id去查询】

categoryServiceImpl.java

1
2
3
4
5
6
@Override
public List<Category> list() {
Map<String, Object> map = ThreadLocalUtil.get();
Integer userId = (Integer) map.get("id");
return categoryMapper.list(userId);
}

categoryMapper

1
2
@Select("select * from category where create_user=#{userId}")
List<Category> list(Integer userId);

测试:

image-20240417105800821

这里的时间格式有点问题,不是例如2024-4-17 11:01:10这样的,所以我们需要在实体类中添加一下注解规范一下时间的格式:

1
2
3
4
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;// 创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;// 更新时间

image-20240417110710285

再次测试时间格式就正常了

获取文章分类详情

请求路径:/category/detail

请求方式:GET

接口描述:该接口用于根据ID获取文章分类详情

更新文章分类

请求路径:/category

请求方式:PUT

接口描述:该接口用于更新文章分类

  • id不能为空
  • categoryName不能为空
  • categoryAlias不能为空

这里需要注意请求体为json格式的,我们需要进行校验,防止其中改的参数为空,所以我们需要在实体类Category中添加相应的校验规则

这里需要添加一个id不为空的校验规则,这样就会导致前面新增文章分类的时候出现id不能为空的报错,因为那个接口中不需要传递id参数,id是由MySQL主键进行自增的,所以这里引出一个规则分组校验

分组校验

把校验项进行归类分组,在完成不同的功能时,校验指定组中的校验项:

  • 定义分组
  • 定义校验项时制定归属的分组
  • 校验时制定要校验的分组

image-20240417140350672

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Category {
@NotNull(groups = Update.class) // 不能不传
private Integer id;// 主键ID
@NotEmpty(groups = {Add.class, Update.class}) // 必须传 如果是字符串不能为空
private String categoryName;// 分类名称
@NotEmpty(groups = {Add.class, Update.class}) // 必须传 如果是字符串不能为空
private String categoryAlias;// 分类别名
private Integer createUser;// 创建人ID
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;// 创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;// 更新时间

public interface Add {

}

public interface Update {

}
}

image-20240417142220635

删除文章分类

请求路径:/category

请求方式:DELETE

接口描述:该接口用于根据ID删除文章分类

【已完成】

文章管理

发布文章(新增)

请求路径:/article

请求方式:POST

接口描述:该接口用于新增文章(发布文章)

请求参数为application/json格式

样例:

1
2
3
4
5
6
7
{
"title": "陕西旅游攻略",
"content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
"coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
"state": "草稿",
"categoryId": 2
}

这里需要注意参数的校验:

  • title为1-10个非空字符
  • coverImg为url地址
  • state必须是 已发布或者草稿

前两个都很好校验,state需要去编写自定义的校验规则注解:

  • 自定义校验

image-20240417171328385

文章列表查询(条件分页)

image-20240418105248483

image-20240418105707617

文件上传

image-20240418135028776

文件上传如果发生同名的问题需要保证文件的名字是唯一的,防止发生文件覆盖,使用uuid对文件进行重命名

OSS 对象存储服务,Object Storage Service

image-20240418161254400

融合OSS对象存储编写工具类

登录优化-Redis

令牌主动失效机制:

  • 登录成功后,给浏览器响应令牌的同时,把该令牌存储到redis中
  • LoginInterceptor拦截器中,需要验证浏览器携带的令牌,并同时需要获取到redis中存储的与之相同的令牌
  • 当用户修改密码成功后,删除redis中存储的旧令牌

集成Redis

image-20240418213211652

引入依赖坐标

1
2
3
4
5
<!--redis依赖坐标-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

修改配置文件

1
2
3
4
data:
redis:
host: localhost
port: 6379

令牌主动失效机制:

  • 登录成功后给浏览器响应令牌的同时,把令牌存储到redis中
  • LoginInterceptor拦截器中,需要验证浏览器携带的令牌,并同时需要获取到redis中存储的与之相同的令牌
  • 当用户修改密码成功后,删除redis中存储的旧令牌

Springboot项目部署

image-20240418221106865

属性配置方式:

面试篇