新增全局异常处理和幂等性相关功能

- 添加全局异常处理器 GlobalExceptionHandler,统一处理系统异常- 新增 HTTP 状态码常量类 HttpStatus
- 实现幂等性测试控制器 IdempotenceController,包括插入、更新和 Token验证操作
- 优化 TestController,调整请求映射为 POST 方法
- 新增 pom.xml 文件,配置 Spring Boot 项目依赖,包括 Knife4j、FastJson、hutool、AOP、MySQL 连接器、RabbitMQ 和 Redis 等
- 创建 SpringDemoApplication 启动类,启动 Spring Boot 应用程序
This commit is contained in:
whaifree 2024-10-12 11:52:40 +08:00
parent c73979681f
commit f768ea78f0
9 changed files with 636 additions and 3 deletions

View File

@ -0,0 +1,30 @@
package cn.whaifree.redo.redo_all_240924;
import java.util.Arrays;
/**
* @version 1.0
* @Author whai文海
* @Date 2024/10/11 11:49
* @注释
*/
public class LeetCode274 {
class Solution {
public int hIndex(int[] citations) {
Arrays.sort(citations);
// 0 1 3 5 6
// | i | h |
for (int i = 0; i < citations.length; i++) {
int h = citations.length - i;
if (citations[i] >= h) {
return h;
}
}
return 0;
}
}
}

119
springDemo/pom.xml Normal file
View File

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.whaifree</groupId>
<artifactId>springDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springDemo</name>
<description>springDemo</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<!--FastJson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.36</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.13</version>
</dependency>
<!--aspect-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<artifactId>mysql-connector-java</artifactId>
<!--jdbc-->
<groupId>mysql</groupId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,13 @@
package cn.whaifree.springdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDemoApplication.class, args);
}
}

View File

@ -0,0 +1,23 @@
package cn.whaifree.springdemo.config;
import cn.whaifree.springdemo.utils.ResVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @version 1.0
* @Author whai文海
* @Date 2024/10/11 16:28
* @注释
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler{
@ExceptionHandler(Exception.class)
public ResVo handleException(Exception e) {
log.error("系统异常", e);
return ResVo.error(e.getMessage());
}
}

View File

@ -2,8 +2,8 @@ package cn.whaifree.springdemo.controller;
import cn.whaifree.springdemo.aspect.annotation.RateLimiter; import cn.whaifree.springdemo.aspect.annotation.RateLimiter;
import cn.whaifree.springdemo.constant.LimitType; import cn.whaifree.springdemo.constant.LimitType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@ -17,7 +17,7 @@ import java.util.concurrent.*;
*/ */
@RestController @RestController
public class TestController { public class TestController {
@RequestMapping("/test") @PostMapping("/test")
@RateLimiter(key = "test:", time = 10, count = 1) // 10s只能1次 @RateLimiter(key = "test:", time = 10, count = 1) // 10s只能1次
public String test() { public String test() {
return "test"; return "test";
@ -31,7 +31,7 @@ public class TestController {
* *
* @return * @return
*/ */
@RequestMapping("addContainer") @PostMapping("addContainer")
@RateLimiter(key = "addContainer:", limitType = LimitType.USER, time = 5, count = 1) // 10s只能1次 @RateLimiter(key = "addContainer:", limitType = LimitType.USER, time = 5, count = 1) // 10s只能1次
public String addContainerInstanceToCluster(@RequestBody List<String> instances, int userId) { public String addContainerInstanceToCluster(@RequestBody List<String> instances, int userId) {
// 导入容器节点到集群中 // 导入容器节点到集群中

View File

@ -0,0 +1,135 @@
package cn.whaifree.springdemo.controller.idempotence;
import cn.whaifree.springdemo.utils.ResVo;
import com.alibaba.fastjson.JSON;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.sql.PreparedStatement;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 幂等性测试
*
* @version 1.0
* @Author whai文海
* @Date 2024/10/11 16:06
* @注释
*/
@RestController
@RequestMapping("/idempotence")
public class IdempotenceController {
@Resource
JdbcTemplate jdbcTemplate;
@Resource
RedisTemplate<String, Object> redisTemplate;
@PostMapping("query")
public ResVo query() {
List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from orders");
return ResVo.success(JSON.toJSONString(maps));
}
/**
* CREATE TABLE `orders` (
* `id` INT AUTO_INCREMENT PRIMARY KEY,
* `order_no` VARCHAR(100) UNIQUE NOT NULL COMMENT '订单号', -- 唯一
* `user_id` INT NOT NULL COMMENT '用户ID',
* `total_amount` DECIMAL(10,2) NOT NULL COMMENT '订单总金额',
* `status` TINYINT DEFAULT 0 COMMENT '订单状态: 0-未支付, 1-已支付',
* `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
* );
* <p>
* java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '1' for key 'order_no'
* at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:118) ~[mysql-connector-j-8.0.33.jar:8.0.33]
* at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-j-8.0.33.jar:8.0.33]
*
* @param orderId
* @param userId
* @param totalAmount
* @return
*/
@PostMapping("insertByMySQL")
public ResVo insertMySQL(String orderId, String userId, Integer totalAmount) {
int update = jdbcTemplate.update(con -> {
PreparedStatement preparedStatement = con.prepareStatement(
"INSERT INTO orders (order_no, user_id, total_amount) " +
"VALUES (?, ?, ?);"
);
preparedStatement.setString(1, orderId);
preparedStatement.setString(2, userId);
preparedStatement.setString(3, String.valueOf(totalAmount));
return preparedStatement;
});
return update > 0 ? ResVo.success("插入成功") : ResVo.error("插入失败");
}
/**
* 适用于更新操作
*
* @return
*/
@PostMapping("upadteCAS")
public ResVo updateCAS(String orderId, Integer totalAmount, Integer originVersion) {
int update = jdbcTemplate.update(con -> {
PreparedStatement preparedStatement = con.prepareStatement(
"UPDATE orders " +
"SET total_amount = ?, version=version+1 " +
"WHERE order_no = ? AND version = ?");
preparedStatement.setInt(1, totalAmount);
preparedStatement.setString(2, orderId);
preparedStatement.setInt(3, originVersion);
return preparedStatement;
});
return update > 0 ? ResVo.success("更新成功") : ResVo.error("更新失败");
}
final String tokenV = "tokenV";
/**
* Token防刷
*/
@PostMapping("token")
public ResVo generateToken() {
// 生成UUID
String s = UUID.randomUUID().toString();
String key = "token:" + s;
redisTemplate.opsForValue().set(key, tokenV, 60, TimeUnit.SECONDS);
return ResVo.success(key);
}
@PostMapping("tokenCheck")
public ResVo tokenCheck(String key) {
String script =
// 有这个token则通过并且删除只能一次性使用
"if redis.call('get',KEYS[1]) == ARGV[1] " + // key argv为参数
"then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 end";
DefaultRedisScript<Long> tDefaultRedisScript = new DefaultRedisScript<>(script, Long.class);
Long execute = redisTemplate.execute(tDefaultRedisScript, Arrays.asList(key), tokenV);
if (execute == 0) {
throw new RuntimeException("未通过");
}
return ResVo.success("通过");
}
}

View File

@ -0,0 +1,196 @@
package cn.whaifree.springdemo.utils;
/**
* @version 1.0
* @Author whai文海
* @Date 2024/10/11 16:30
* @注释
*/
import cn.whaifree.springdemo.utils.constants.HttpStatus;
import java.util.HashMap;
/**
* 操作消息提醒
*
* @author /
*/
public class ResVo extends HashMap<String, Object>
{
private static final long serialVersionUID = 1L;
/** 状态码 */
public static final String CODE_TAG = "code";
/** 返回内容 */
public static final String MSG_TAG = "msg";
/** 数据对象 */
public static final String DATA_TAG = "data";
/**
* 初始化一个新创建的 ResVo 对象使其表示一个空消息
*/
public ResVo()
{
}
/**
* 初始化一个新创建的 ResVo 对象
*
* @param code 状态码
* @param msg 返回内容
*/
public ResVo(int code, String msg)
{
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
}
public boolean isEmpty(){
return super.get(CODE_TAG).equals(HttpStatus.FOUND);
}
/**
* 初始化一个新创建的 ResVo 对象
*
* @param code 状态码
* @param msg 返回内容
* @param data 数据对象
*/
public ResVo(int code, String msg, Object data)
{
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (data != null) {
super.put(DATA_TAG, data);
}
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static ResVo success()
{
return ResVo.success("操作成功");
}
/**
* 返回成功数据
*
* @return 成功消息
*/
public static ResVo success(Object data)
{
return ResVo.success("操作成功", data);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @return 成功消息
*/
public static ResVo success(String msg)
{
return ResVo.success(msg, null);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 成功消息
*/
public static ResVo success(String msg, Object data)
{
return new ResVo(HttpStatus.SUCCESS, msg, data);
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static ResVo warn(String msg)
{
return ResVo.warn(msg, null);
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static ResVo warn(String msg, Object data)
{
return new ResVo(HttpStatus.WARN, msg, data);
}
/**
* 返回错误消息
*
* @return 错误消息
*/
public static ResVo error()
{
return ResVo.error("操作失败");
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @return 错误消息
*/
public static ResVo error(String msg)
{
return ResVo.error(msg, null);
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 错误消息
*/
public static ResVo error(String msg, Object data)
{
return new ResVo(HttpStatus.ERROR, msg, data);
}
/**
* 返回错误消息
*
* @param code 状态码
* @param msg 返回内容
* @return 错误消息
*/
public static ResVo error(int code, String msg)
{
return new ResVo(code, msg, null);
}
/**
* 方便链式调用
*
* @param key
* @param value
* @return 数据对象
*/
@Override
public ResVo put(String key, Object value)
{
super.put(key, value);
return this;
}
}

View File

@ -0,0 +1,95 @@
package cn.whaifree.springdemo.utils.constants;
/**
* @version 1.0
* @Author whai文海
* @Date 2024/10/11 16:32
* @注释
*/
public class HttpStatus
{
/**
* 操作成功
*/
public static final int SUCCESS = 0;
/**
* 对象创建成功
*/
public static final int CREATED = 201;
/**
* 请求已经被接受
*/
public static final int ACCEPTED = 202;
/**
* 操作已经执行成功但是没有返回数据
*/
public static final int NO_CONTENT = 204;
/**
* 资源已被移除
*/
public static final int MOVED_PERM = 301;
/**
* 重定向
*/
public static final int SEE_OTHER = 303;
/**
* 资源没有被修改
*/
public static final int NOT_MODIFIED = 304;
/**
* 参数列表错误缺少格式不匹配
*/
public static final int BAD_REQUEST = 400;
/**
* 未授权
*/
public static final int UNAUTHORIZED = 401;
/**
* 访问受限授权过期
*/
public static final int FORBIDDEN = 403;
/**
* 资源服务未找到
*/
public static final int FOUND = 40004;
/**
* 不允许的http方法
*/
public static final int BAD_METHOD = 405;
/**
* 资源冲突或者资源被锁
*/
public static final int CONFLICT = 409;
/**
* 不支持的数据媒体类型
*/
public static final int UNSUPPORTED_TYPE = 415;
/**
* 系统内部错误
*/
public static final int ERROR = 500;
/**
* 接口未实现
*/
public static final int NOT_IMPLEMENTED = 501;
/**
* 系统警告消息
*/
public static final int WARN = 601;
}

View File

@ -1,3 +1,4 @@
spring: spring:
datasource: datasource:
url: jdbc:mysql://localhost:3306/springTest?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai url: jdbc:mysql://localhost:3306/springTest?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
@ -11,3 +12,24 @@ spring:
port: 6379 port: 6379
# 选择db1 # 选择db1
database: 3 database: 3
# springdoc-openapi项目配置
springdoc:
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
api-docs:
path: /v3/api-docs
group-configs:
- group: 'default'
paths-to-match: '/**'
packages-to-scan: cn.whaifree.springdemo.controller
# knife4j的增强配置不需要增强可以不配
knife4j:
enable: true
setting:
language: zh_cn
server:
port: 8080