前言

注意

  • 项目为了练习,没有使用mybatisplus的简化写法

统一错误(异常)处理

  • 还是这位博主的博客

  • 使用统一结果处理时,有些异常我们可以提前预知并处理,但是一个运行时异常,我们不一定能预知并处理,这时可以使用统一异常处理,当异常发生时,触发该处理操作,从而保证程序的健壮性。

  • 使用 @ControllerAdvice 或者 @RestControllerAdvice 注解作为统一异常处理的核心。

    • 这两个注解都是 Spring MVC 提供的。作用于 控制层 的一种切面通知。
1
2
3
4
【@ControllerAdvice 与 @RestControllerAdvice 区别:】
@RestControllerAdvice 注解包含了 @ControllerAdvice 与 @ResponseBody 注解。
类似于 @Controller 与 @RestController 的区别。
@RestControllerAdvice 返回 json 数据时不需要添加 @ResponseBody 注解。

自定义一个异常类,用于处理项目中的异常,并收集异常信息。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package com.lyh.common.exception;

import lombok.Data;
import org.apache.http.HttpStatus;

/**
* 自定义异常,
* 可以自定义 异常信息 message 以及 响应状态码 code(默认为 500)。
*
* 依赖信息说明:
* 此处使用 @Data 注解,需导入 lombok 相关依赖文件。
* 使用 HttpStatus 的常量表示 响应状态码,需导入 httpcore 相关依赖文件。
*/
@Data
public class GlobalException extends RuntimeException {
/**
* 保存异常信息
*/
private String message;

/**
* 保存响应状态码
*/
private Integer code = HttpStatus.SC_INTERNAL_SERVER_ERROR;

/**
* 默认构造方法,根据异常信息 构建一个异常实例对象
* @param message 异常信息
*/
public GlobalException(String message) {
super(message);
this.message = message;
}

/**
* 根据异常信息、响应状态码构建 一个异常实例对象
* @param message 异常信息
* @param code 响应状态码
*/
public GlobalException(String message, Integer code) {
super(message);
this.message = message;
this.code = code;
}

/**
* 根据异常信息,异常对象构建 一个异常实例对象
* @param message 异常信息
* @param e 异常对象
*/
public GlobalException(String message, Throwable e) {
super(message, e);
this.message = message;
}

/**
* 根据异常信息,响应状态码,异常对象构建 一个异常实例对象
* @param message 异常信息
* @param code 响应状态码
* @param e 异常对象
*/
public GlobalException(String message, Integer code, Throwable e) {
super(message, e);
this.message = message;
this.code = code;
}
}

再定义一个全局的异常处理类 GlobalExceptionHandler。

  • 使用 @RestControllerAdvice 注解标记这个类。
  • 内部使用 @ExceptionHandler 注解去捕获异常。
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
40
41
42
43
44
45
46
47
48
49
50
package com.lyh.common.exception;

import com.lyh.common.util.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
* 全局异常处理类。
* 使用 slf4j 保存日志信息。
* 此处使用了 统一结果处理 类 Result 用于包装异常信息。
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());

/**
* 处理 Exception 异常
* @param e 异常
* @return 处理结果
*/
@ExceptionHandler(Exception.class)
public Result handlerException(Exception e) {
logger.error(e.getMessage(), e);
return Result.error().message("系统异常");
}

/**
* 处理空指针异常
* @param e 异常
* @return 处理结果
*/
@ExceptionHandler(NullPointerException.class)
public Result handlerNullPointerException(NullPointerException e) {
logger.error(e.getMessage(), e);
return Result.error().message("空指针异常");
}

/**
* 处理自定义异常
* @param e 异常
* @return 处理结果
*/
@ExceptionHandler(GlobalException.class)
public Result handlerGlobalException(GlobalException e) {
logger.error(e.getMessage(), e);
return Result.error().message(e.getMessage()).code(e.getCode());
}
}

使用

1
2
3
4
5
6
使用?
修改某个 controller 如下所示:
参数不存在时,抛出 空指针异常。
参数为 -1 时,抛出自定义异常并处理。
查询结果为 null 时,抛出自定义异常并处理。
查询成功时,正确处理并返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("selectOne")
public Result selectOne(Integer id) {
Emp emp = this.empService.queryById(id);
if (id == null) {
throw new NullPointerException();
}
if (id == -1) {
throw new GlobalException("参数异常", 400);
}
if (emp == null) {
throw new GlobalException("未查询到结果,请确认输入是否正确");
}
return Result.ok().data("items", emp).message("查询成功");
}

统一结果处理

数据格式?

  • 是否响应成功(success: true / false)
  • 响应状态码(code:200 / 400 / 500 等)
  • 状态码描述(message:访问成功 / 系统异常等)
  • 响应数据(data:处理的数据)

如何处理

  • success 设置成 Boolean 类型。

  • code 设置成 Integer类型。  

  • message 设置成 String类型。

  • data 设置成 HashMap 类型。

  • 构造器私有,且使用静态方法返回类对象。

  • 采用链式调用(即方法返回对象为其本身,return thi

  • 所以打算直接拿这位博主的来~

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpcore</artifactId>
    <version>4.4.16</version>
    </dependency>
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package com.lyh.common.util;

import lombok.Data;
import org.apache.http.HttpStatus;

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

/**
* 统一结果返回类。方法采用链式调用的写法(即返回类本身 return this)。
* 构造器私有,不允许进行实例化,但提供静态方法 ok、error 返回一个实例。
* 静态方法说明:
* ok 返回一个 成功操作 的结果(实例对象)。
* error 返回一个 失败操作 的结果(实例对象)。
*
* 普通方法说明:
* success 用于自定义响应是否成功
* code 用于自定义响应状态码
* message 用于自定义响应消息
* data 用于自定义响应数据
*
* 依赖信息说明:
* 此处使用 @Data 注解,需导入 lombok 相关依赖文件。
* 使用 HttpStatus 的常量表示 响应状态码,需导入 httpcore 相关依赖文件。
*/
@Data
public class Result {
/**
* 响应是否成功,true 为成功,false 为失败
*/
private Boolean success;

/**
* 响应状态码, 200 成功,500 系统异常
*/
private Integer code;

/**
* 响应消息
*/
private String message;

/**
* 响应数据
*/
private Map<String, Object> data = new HashMap<>();

/**
* 默认私有构造器
*/
private Result(){}

/**
* 私有自定义构造器
* @param success 响应是否成功
* @param code 响应状态码
* @param message 响应消息
*/
private Result(Boolean success, Integer code, String message){
this.success = success;
this.code = code;
this.message = message;
}

/**
* 返回一个默认的 成功操作 的结果,默认响应状态码 200
* @return 成功操作的实例对象
*/
public static Result ok() {
return new Result(true, HttpStatus.SC_OK, "success");
}

/**
* 返回一个自定义 成功操作 的结果
* @param success 响应是否成功
* @param code 响应状态码
* @param message 响应消息
* @return 成功操作的实例对象
*/
public static Result ok(Boolean success, Integer code, String message) {
return new Result(success, code, message);
}

/**
* 返回一个默认的 失败操作 的结果,默认响应状态码为 500
* @return 失败操作的实例对象
*/
public static Result error() {
return new Result(false, HttpStatus.SC_INTERNAL_SERVER_ERROR, "error");
}

/**
* 返回一个自定义 失败操作 的结果
* @param success 响应是否成功
* @param code 响应状态码
* @param message 相应消息
* @return 失败操作的实例对象
*/
public static Result error(Boolean success, Integer code, String message) {
return new Result(success, code, message);
}

/**
* 自定义响应是否成功
* @param success 响应是否成功
* @return 当前实例对象
*/
public Result success(Boolean success) {
this.setSuccess(success);
return this;
}

/**
* 自定义响应状态码
* @param code 响应状态码
* @return 当前实例对象
*/
public Result code(Integer code) {
this.setCode(code);
return this;
}

/**
* 自定义响应消息
* @param message 响应消息
* @return 当前实例对象
*/
public Result message(String message) {
this.setMessage(message);
return this;
}

/**
* 自定义响应数据,一次设置一个 map 集合
* @param map 响应数据
* @return 当前实例对象
*/
public Result data(Map<String, Object> map) {
this.data.putAll(map);
return this;
}

/**
* 通用设置响应数据,一次设置一个 key - value 键值对
* @param key 键
* @param value 数据
* @return 当前实例对象
*/
public Result data(String key, Object value) {
this.data.put(key, value);
return this;
}
}

前端传参校验

Post用法

  • 在bean层添加对应注解
1
2
3
4
5
6
7
8
public class demo{
@NotNull(message="用户id不能为空")
private Long userId;
@NotBlank(message="用户名不能为空")
private String userName;
@NotBlank(message="年龄不能为空")
private String age;
}
  • 在controller层添加

    • @Valid 注解用于告诉 Spring Boot 对 MyRequest 对象进行验证
    1
    2
    3
    4
    5
    6
    @PostMapping("/xxx")
    public String createDemo(@RequestBody @Valid Demo demo, BindingResult result){
    if(result.hasErrors())
    return result.getFieldError().getDefaultMessage();
    return "sucess";
    }

Get用法

  • 注意,如果需要使用校验,那么就需要关闭必填项@RequestParam(value = "username",required = false),否则就会出现org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'username' for method parameter type String is not present,也就是字段未填写的报错

  • controller代码

1
2
3
4
5
6
@GetMapping("/test2")
public String getUserStr(
@RequestParam(value = "username",required = false) @NotNull(message = "名字不能为空") String name) {

return "success";
}
  • 测试

登录态

  • 这里就用jwt来生成一个token并添加在token里面
  • jwt依赖
1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
* jwt工具类
*/
public class JwtUtil {

private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
private static final String SECRET = "S/4AN9IsSRUC~{0c4]y#$F2XbV8^`#a14vawn<~Kr@(D%3TF-p1s/h{Y9k7y((rR";
private static final long defaultExpire = 1000 * 60 * 60 * 24 * 7L;//7天
//创建一个jwt密钥 加密和解密都需要用这个玩意
private static final SecretKey key = Jwts.SIG.HS256.key()
.random(new SecureRandom(SECRET.getBytes(StandardCharsets.UTF_8)))
.build();

private JwtUtil() {
}

/**
* 使用默认过期时间(7天),生成一个JWT
*
* @param username 用户名
* @param claims JWT中的数据
* @return
*/
public static String createToken(String username, Map<String, Object> claims) {
return createToken(username, claims, defaultExpire);
}

/**
* 生成token
*
* @param username 用户名
* @param claims 请求体数据
* @param expire 过期时间 单位:毫秒
* @return token
*/
public static String createToken(String username, Map<String, Object> claims, Long expire) {
JwtBuilder builder = Jwts.builder();
Date now = new Date();
// 生成token
builder.id("rQRk$yN:7%*Bw}A_A-]M~4#;yGa:a_F{") //id 这个可以不填,但是建议填
.issuer("Galaxy") //签发者
.claims(claims) //数据
.subject(username) //主题
.issuedAt(now) //签发时间
.expiration(new Date(now.getTime() + expire)) //过期时间
.signWith(key); //签名方式
builder.header().add("JWT", "JSpWdhuPGblNZApVclmX");
return builder.compact();
}

/**
* 解析token
*
* @param token jwt token
* @return Claims
*/
public static Claims claims(String token) {
try {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (Exception e) {
if (e instanceof ExpiredJwtException) {
//现在不需要使用 claims.getExpiration().before(new Date());
// 判断JWT是否过期了 如果过期会抛出ExpiredJwtException异常
throw new RunException("token已过期");
}
if (e instanceof JwtException) {
throw new RunException("token已失效");
}
logger.error("jwt解析失败" + e);
throw new RunException("token解析失败");
}
}


public static void main(String[] args) {
Map<String, Object> claims = Map.of("name", "张三");
String token = createToken("mysterious", claims, 3L);
System.out.println(token);
Claims claims1 = claims(token);
System.out.println(claims1);
}
}

token续签?到时续期?

redis

jwt过期判断

1
2
3
4
5
6
7
8
void testJWT1(){
String token = "eyJhbGciOiJIUzI1NiIsInR5cGUiOiJKV1QiLCJ0eXAiOiJKV1QifQ.eyJpYXQiOjE3MTE1Mjg2NjYsImV4cCI6MTcxMTUyODY3NiwianRpIjoiM2RkN2IzNmUtNjQxMS00OGEzLTkwMDMtZDVhZGVlZWQ1OTBhIiwic3ViIjoiYXV0aCIsInVzZXJOYW1lIjoicWl1eWUiLCJpZCI6MTIzfQ.lPbcvtgbI4VEhQZrkhmws-zmZLXlQrysRlAmFnNsVeU";
try {
JWTValidator jwtValidator = JWTValidator.of(token).validateDate(DateUtil.date());
} catch (ValidateException exception) {
throw new JWTException("token已过期");
}
}

登录注册

用户注册(需返回主键)

分页功能

  • 参考文章
1
2
3
4
5
6
7
@Test
//测试分页
void testPage(){
PageHelper.startPage(1,10);
List<ArticleInfo> articleInfos = articleInfoMapper.articleList();
PageInfo<ArticleInfo> articleInfoPageInfo = new PageInfo<>(articleInfos);
}
  • 下图展示的是articleInfoPageInfo的信息

问题

  • cn.hutool.core.convert.NumberWithFormat cannot be cast to java.lang.Integer

    • 使用Integer.parseInt方法转换
  • 需要的类型:Supplier<java.lang.String>提供的类型:String(Required type: Supplier <java.lang.String> Provided: String)

    • 导错误包了,应该是slf4j的
    1
    2
    3
    4
    5
    // import org.mybatis.logging.Logger;
    // import org.mybatis.logging.LoggerFactory;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

  • mybatis中的@param什么时候加什么时候不加呢

  • @RequestBody通常,在处理 HTTP POST 请求时,客户端将请求的数据作为请求体发送到服务器端。服务器端可以使用@RequestBody 注解将请求体的内容绑定到方法的参数上,以便进行处理。

在上述示例中,@RequestBody 注解应用于 User 对象的参数,表示将请求体的内容绑定到 User 对象上。这样,当客户端发送一个包含用户信息的 JSON 请求体时,Spring 框架会自动将该 JSON 数据转换为 User 对象,并将其作为参数传递给 createUser 方法。

需要注意的是,使用 @RequestBody 注解时,Spring 框架会使用消息转换器(MessageConverter)来处理请求体的数据转换。默认情况下,Spring 支持多种消息转换器,包括处理 JSON、XML、Form 表单等数据格式。

总结来说,@RequestBody 注解用于将 HTTP 请求的内容绑定到方法的参数上,方便在 Spring 控制器中处理请求体的数据。

1
2
3
4
@PostMapping("/users")
public ResponseEntity createUser(@RequestBody User user) {
// 处理创建用户的逻辑
}
1
2
3
4
5
6
总结
@requestParam
1.用来获取URL后面追加的参数
2.POST请求,content-type:application/json 的body中的参数
@requestBoby
1.接收POST ,content-type:application/json的body参数 (后端一般封装成javaBean对象处理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
问题代码
@PostMapping( "/register")
public Result register(@RequestBody UserInfo userInfo){
log.info(userInfo.toString());
return Result.ok();
}
前端接口Content-Type:application/x-www-form-urlencoded

原因
前端请求传Json对象的字符串则后端才使用@RequestBody。而我前端采用的表单提交的数据,是不能采用@RequestBody注解的。

解决办法:
第一种解决方式就是修改后端代码,去掉@RequestBody注解,也可以直接获取到表单提交的POST数据。
//修改后
@PostMapping( "/register")
public Result register(UserInfo userInfo){
log.info(userInfo.toString());
return Result.ok();
}

第二种解决方式就是修改前端代码,前端传递JSON对象的字符串,这里我采用jQuery来发送ajax请求传递JSON对象的字符串数据。
指定contentType: "application/json",