Compare commits

..

25 Commits

Author SHA1 Message Date
6ad9e08c6b release: v2.10.0 2025-03-26 22:43:13 +08:00
liquor
1613374fcc fix(log): 修复构建请求可重复读流后过滤文件流导致的错误问题 2025-03-26 14:41:36 +00:00
e294d69cdb refactor(log): 优化访问日志启用属性名 2025-03-26 21:29:30 +08:00
89a347ffe6 build: snail-job 1.3.0 => 1.4.0 2025-03-26 21:20:52 +08:00
92f5ea799e fix(cache/redisson): 修复嵌套属性未添加注解导致无法注入的问题 2025-03-26 20:44:00 +08:00
21262701dc refactor(web): 请求响应可重复读流处理由 core 调整到 web 模块 2025-03-26 20:43:16 +08:00
85285e56a8 feat(trace): 新增链路追踪模块(原 web 模块内组件) 2025-03-26 20:41:46 +08:00
b5bfe5c681 feat(security/xss): 新增 XSS 过滤模块(原 web 模块内组件) 2025-03-26 20:41:20 +08:00
3fc9d1fbaa build(log): 移除 Web 模块依赖 2025-03-25 22:27:24 +08:00
4c385927b4 refactor(log): 优化访问日志相关配置属性名称 2025-03-25 22:16:43 +08:00
liquor
da5e162a2a feat(core): 新增请求响应可重复读流处理并优化日志模块
增加访问日志打印处理:包括参数打印、过滤敏感参数和超长参数配置
2025-03-25 13:09:06 +00:00
1903520433 refactor(extension/crud): 将新增操作由 ADD 改为创建操作 CREATE 2025-03-21 20:46:07 +08:00
d9ac2764aa feat(web): 新增日期类型转换器 2025-03-20 20:30:23 +08:00
c9c7c34506 refactor: 优化部分错误提示信息和代码注释 2025-03-18 20:42:46 +08:00
a6fb65f97e feat(core): 添加手机号校验注解并优化枚举校验提示信息
- 新增 @Mobile 注解用于手机号格式校验
- 修改 @EnumValue 注解的默认提示信息为 "参数值无效"
2025-03-18 20:37:47 +08:00
f50b511513 refactor(idempotent):调整 @Idempotent 注解的默认超时时间 2025-03-18 20:21:06 +08:00
071aef759a docs: 更新 README 文档 2025-03-17 21:57:12 +08:00
2b3de0c67e refactor(ratelimiter): 将限流相关代码从 security 模块中分离,创建独立的 ratelimiter 模块
修复部分幂等配置错误
2025-03-17 21:53:26 +08:00
27a71cf076 refactor(idempotent): 重构幂等模块并支持 Redisson 缓存 2025-03-17 21:25:21 +08:00
b199b651ec feat(cache/redisson): 添加条件性缓存设置方法 setIfAbsent、setIfExists 2025-03-17 21:24:35 +08:00
974efa368a build(dependencies): 更新项目依赖版本
- 项目版本号 2.9.0 => 2.10.0-SNAPSHOT
- Spring Boot 3.2.12 => 3.3.9
- 新增 Spring Cloud 2023.0.5
- Redisson 3.41.0 => 3.45.0
- CosId 2.10.1 => 2.11.0
- sa-token 1.39.0 => 1.40.0
- snail-job 1.2.0 => 1.3.0
- sms4j 3.3.3 => 3.3.4
- nashorn 15.5 => 15.6
- s3 2.29.23 => 2.30.35
- s3-crt 0.33.5 => 0.36.1
- ip2region 3.2.12 => 3.3.6
- hutool 5.8.34 => 5.8.36
- mybatis-flex 1.10.3 => 1.10.8
- snakeyaml 2.3 => 2.4
- flatten 1.6.0 => 1.7.0
- spotless 2.43.0 => 2.44.3
2025-03-16 21:29:10 +08:00
Gyq灬明
199df874e5 feat(idempotent): 新增幂等模块
实现内存/redis 模式幂等(默认内存模式)
2025-03-10 06:37:56 +00:00
591a44d861 refactor(crud): 将详情方法命名还原为 get
- 修改了 AbstractBaseController 中的 detail 方法名为 get
- 更新了对应的 Api 枚举值,将 DETAIL 改为 GET
2025-03-07 22:44:10 +08:00
licoocn
e7d99e65aa fix(web): 优化默认全局响应实体 R ,为 status 字段添加默认值 DefaultResponseStatus 2025-03-06 09:58:35 +00:00
0d7f777fd5 fix(web): 修复开启 i18n 后访问接口报错的问题 2025-02-20 20:36:55 +08:00
100 changed files with 2009 additions and 395 deletions

View File

@@ -1,3 +1,47 @@
## [v2.10.0](https://github.com/continew-org/continew-starter/compare/v2.9.0...v2.10.0) (2025-03-26)
### ✨ 新特性
- 【idempotent】新增幂等模块 (Gitee#41@aiming317) ([199df87](https://github.com/continew-org/continew-starter/commit/199df874e54207d9b05230dcd2ec83be0e6d3f06)) ([27a71cf](https://github.com/continew-org/continew-starter/commit/27a71cf07675315405908a5befd25ad6e5c7471c)) ([f50b511](https://github.com/continew-org/continew-starter/commit/f50b51151391e36e49f31a0e553d8c86f1827821))
- 【cache/redisson】添加条件性缓存设置方法 setIfAbsent、setIfExists ([b199b65](https://github.com/continew-org/continew-starter/commit/b199b651ecf8a2de6cccafa4efc98c7d65446ebd))
- 【core】添加手机号校验注解并优化枚举校验提示信息 ([a6fb65f](https://github.com/continew-org/continew-starter/commit/a6fb65f97e22ea0e7eec7d9c523e3c550b1d73d0))
- 【web】新增日期类型转换器 ([d9ac276](https://github.com/continew-org/continew-starter/commit/d9ac2764aa78e83a11ee5440155a8cd7bf1cb8c8))
- 【security/xss】新增 XSS 过滤模块(原 web 模块内组件) ([b5bfe5c](https://github.com/continew-org/continew-starter/commit/b5bfe5c6813323d45cd5879a2e0f9bbd88d657e0))
- 【trace】新增链路追踪模块原 web 模块内组件) ([85285e5](https://github.com/continew-org/continew-starter/commit/85285e56a83324d9a6542531dbdf3e82f8af0301))
### 💎 功能优化
- 【extension/crud】将详情方法命名还原为 get ([591a44d](https://github.com/continew-org/continew-starter/commit/591a44d861151b89f1f748d18092b546bb0935e0))
- 【extension/crud】将新增操作由 ADD 改为创建操作 CREATE ([1903520](https://github.com/continew-org/continew-starter/commit/19035204336f0c9d462e75e89561514aa1414f27))
- 【ratelimiter】将限流相关代码从 security 模块中分离,创建独立的 ratelimiter 模块 ([2b3de0c](https://github.com/continew-org/continew-starter/commit/2b3de0c67e1e6f4b29fed4a732a48e5512dad4ac))
- 优化部分错误提示信息和代码注释 ([c9c7c34](https://github.com/continew-org/continew-starter/commit/c9c7c345062a126e802f5d92d06710f503e8f733))
- 【log】重构访问日志 (Gitee#42@dom-w) ([da5e162](https://github.com/continew-org/continew-starter/commit/da5e162a2ab5c4a428bcdda4c8ea94d52722b7ad)) ([4c38592](https://github.com/continew-org/continew-starter/commit/4c385927b4e57402dc06e7713388984ead1186b3)) ([4c38592](https://github.com/continew-org/continew-starter/commit/4c385927b4e57402dc06e7713388984ead1186b3)) ([1613374](https://github.com/continew-org/continew-starter/commit/1613374fcca67381e9fcf6b3677527d66f6ea3db))
### 🐛 问题修复
- 【web】修复开启 i18n 后访问接口报错的问题 ([0d7f777](https://github.com/continew-org/continew-starter/commit/0d7f777fd56e08ef3842521285bb8c379e408874))
- 【web】优化默认全局响应实体 R ,为 status 字段添加默认值 DefaultResponseStatus (Gitee#39@zs3zs) ([e7d99e6](https://github.com/continew-org/continew-starter/commit/e7d99e65aa2a22154a81ba087dbc11a6aee9598f))
- 【cache/redisson】修复嵌套属性未添加注解导致无法注入的问题 ([92f5ea7](https://github.com/continew-org/continew-starter/commit/92f5ea799e9059f8c2e5bef37f0beb4074b894db))
### 📦 依赖升级
- Spring Boot 3.2.12 => 3.3.9 ([974efa3](https://github.com/continew-org/continew-starter/commit/974efa368a983548ac87f0fa4ee4e181a6383668))
- 新增 Spring Cloud 2023.0.5
- Redisson 3.41.0 => 3.45.0
- CosId 2.10.1 => 2.11.0
- sa-token 1.39.0 => 1.40.0
- snail-job 1.2.0 => 1.4.0
- sms4j 3.3.3 => 3.3.4
- nashorn 15.5 => 15.6
- s3 2.29.23 => 2.30.35
- s3-crt 0.33.5 => 0.36.1
- ip2region 3.2.12 => 3.3.6
- hutool 5.8.34 => 5.8.36
- mybatis-flex 1.10.3 => 1.10.8
- snakeyaml 2.3 => 2.4
- flatten 1.6.0 => 1.7.0
- spotless 2.43.0 => 2.44.3
## [v2.9.0](https://github.com/continew-org/continew-starter/compare/v2.8.3...v2.9.0) (2025-02-14)
### ✨ 新特性

View File

@@ -4,7 +4,7 @@
<img src="https://img.shields.io/maven-central/v/top.continew/continew-starter.svg?label=Maven%20Central&logo=sonatype&logoColor=FFF" alt="Release" />
</a>
<a href="https://spring.io/projects/spring-boot" title="Spring Boot" target="_blank">
<img src="https://img.shields.io/badge/Spring Boot-3.2.12-%236CB52D.svg?logo=Spring-Boot" alt="Spring Boot" />
<img src="https://img.shields.io/badge/Spring Boot-3.3.9-%236CB52D.svg?logo=Spring-Boot" alt="Spring Boot" />
</a>
<a href="https://github.com/continew-org/continew-starter" title="Open JDK" target="_blank">
<img src="https://img.shields.io/badge/Open JDK-17-%236CB52D.svg?logo=OpenJDK&logoColor=FFF" alt="Open JDK" />
@@ -166,9 +166,12 @@ continew-starter
│ └─ continew-starter-cache-springcacheSpring 缓存)
├─ continew-starter-security安全模块
│ ├─ continew-starter-security-crypto加密字段加解密
│ ├─ continew-starter-security-xssXSS 过滤)
│ ├─ continew-starter-security-mask脱敏JSON 数据脱敏)
│ ├─ continew-starter-security-limiter限流
│ └─ continew-starter-security-password密码编码器
├─ continew-starter-ratelimiter限流模块
├─ continew-starter-idempotent幂等模块
├─ continew-starter-trace链路追踪模块
├─ continew-starter-captcha验证码模块
│ ├─ continew-starter-captcha-graphic静态验证码
│ └─ continew-starter-captcha-behavior动态验证码

View File

@@ -20,6 +20,7 @@ import org.redisson.config.ClusterServersConfig;
import org.redisson.config.SentinelServersConfig;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* Redisson 配置属性
@@ -44,16 +45,19 @@ public class RedissonProperties {
/**
* 单机服务配置
*/
@NestedConfigurationProperty
private SingleServerConfig singleServerConfig;
/**
* 集群服务配置
*/
@NestedConfigurationProperty
private ClusterServersConfig clusterServersConfig;
/**
* 哨兵服务配置
*/
@NestedConfigurationProperty
private SentinelServersConfig sentinelServersConfig;
/**

View File

@@ -62,6 +62,62 @@ public class RedisUtils {
CLIENT.getBucket(key).set(value, duration);
}
/**
* 设置缓存
*
* <p>如果键已存在,则不设置</p>
*
* @param key 键
* @param value 值
* @return true设置成功false设置失败
* @since 2.10.0
*/
public static <T> boolean setIfAbsent(String key, T value) {
return CLIENT.getBucket(key).setIfAbsent(value);
}
/**
* 设置缓存
*
* <p>如果键已存在,则不设置</p>
*
* @param key 键
* @param value 值
* @param duration 过期时间
* @return true设置成功false设置失败
* @since 2.10.0
*/
public static <T> boolean setIfAbsent(String key, T value, Duration duration) {
return CLIENT.getBucket(key).setIfAbsent(value, duration);
}
/**
* 设置缓存
* <p>如果键不存在,则不设置</p>
*
* @param key 键
* @param value 值
* @return true设置成功false设置失败
* @since 2.10.0
*/
public static <T> boolean setIfExists(String key, T value) {
return CLIENT.getBucket(key).setIfExists(value);
}
/**
* 设置缓存
* <p>如果键不存在,则不设置</p>
*
* @param key 键
* @param value 值
* @param duration 过期时间
* @return true设置成功false设置失败
* @since 2.10.0
*/
public static <T> boolean setIfExists(String key, T value, Duration duration) {
return CLIENT.getBucket(key).setIfExists(value, duration);
}
/**
* 查询指定缓存
*

View File

@@ -23,6 +23,12 @@
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!-- AOPAspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!-- Hibernate Validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>

View File

@@ -40,50 +40,40 @@ public class PropertiesConstants {
public static final String SECURITY = CONTINEW_STARTER + StringConstants.DOT + "security";
/**
* 密码编解码配置
* 安全-密码编解码配置
*/
public static final String SECURITY_PASSWORD = SECURITY + StringConstants.DOT + "password";
/**
* 加/解密配置
* 安全-加/解密配置
*/
public static final String SECURITY_CRYPTO = SECURITY + StringConstants.DOT + "crypto";
/**
* 限流器配置
*/
public static final String SECURITY_LIMITER = SECURITY + StringConstants.DOT + "limiter";
/**
* 敏感词配置
* 安全-敏感词配置
*/
public static final String SECURITY_SENSITIVE_WORDS = SECURITY + StringConstants.DOT + "sensitive-words";
/**
* 安全-XSS 配置
*/
public static final String SECURITY_XSS = SECURITY + StringConstants.DOT + "xss";
/**
* Web 配置
*/
public static final String WEB = CONTINEW_STARTER + StringConstants.DOT + "web";
/**
* 跨域配置
* Web-跨域配置
*/
public static final String WEB_CORS = WEB + StringConstants.DOT + "cors";
/**
* 响应配置
* Web-响应配置
*/
public static final String WEB_RESPONSE = WEB + StringConstants.DOT + "response";
/**
* 链路配置
*/
public static final String WEB_TRACE = WEB + StringConstants.DOT + "trace";
/**
* XSS 配置
*/
public static final String WEB_XSS = WEB + StringConstants.DOT + "xss";
/**
* 日志配置
*/
@@ -94,11 +84,6 @@ public class PropertiesConstants {
*/
public static final String STORAGE = CONTINEW_STARTER + StringConstants.DOT + "storage";
/**
* 本地存储配置
*/
public static final String STORAGE_LOCAL = STORAGE + StringConstants.DOT + "local";
/**
* 验证码配置
*/
@@ -139,6 +124,21 @@ public class PropertiesConstants {
*/
public static final String TENANT = CONTINEW_STARTER + StringConstants.DOT + "tenant";
/**
* 限流配置
*/
public static final String RATE_LIMITER = CONTINEW_STARTER + StringConstants.DOT + "rate-limiter";
/**
* 幂等配置
*/
public static final String IDEMPOTENT = CONTINEW_STARTER + StringConstants.DOT + "idempotent";
/**
* 链路追踪配置
*/
public static final String TRACE = CONTINEW_STARTER + StringConstants.DOT + "trace";
private PropertiesConstants() {
}
}

View File

@@ -44,7 +44,7 @@ public class Validator {
* 如果为空,抛出异常
*
* @param obj 被检测的对象
* @param message 错误信息
* @param message 提示信息
* @param exceptionType 异常类型
*/
protected static void throwIfNull(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
@@ -55,7 +55,7 @@ public class Validator {
* 如果不为空,抛出异常
*
* @param obj 被检测的对象
* @param message 错误信息
* @param message 提示信息
* @param exceptionType 异常类型
*/
protected static void throwIfNotNull(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
@@ -66,7 +66,7 @@ public class Validator {
* 如果为空,抛出异常
*
* @param obj 被检测的对象
* @param message 错误信息
* @param message 提示信息
* @param exceptionType 异常类型
*/
protected static void throwIfEmpty(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
@@ -77,7 +77,7 @@ public class Validator {
* 如果不为空,抛出异常
*
* @param obj 被检测的对象
* @param message 错误信息
* @param message 提示信息
* @param exceptionType 异常类型
*/
protected static void throwIfNotEmpty(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
@@ -88,7 +88,7 @@ public class Validator {
* 如果为空,抛出异常
*
* @param str 被检测的字符串
* @param message 错误信息
* @param message 提示信息
* @param exceptionType 异常类型
*/
protected static void throwIfBlank(CharSequence str,
@@ -101,7 +101,7 @@ public class Validator {
* 如果不为空,抛出异常
*
* @param str 被检测的字符串
* @param message 错误信息
* @param message 提示信息
* @param exceptionType 异常类型
*/
protected static void throwIfNotBlank(CharSequence str,
@@ -115,7 +115,7 @@ public class Validator {
*
* @param obj1 要比较的对象1
* @param obj2 要比较的对象2
* @param message 错误信息
* @param message 提示信息
* @param exceptionType 异常类型
*/
protected static void throwIfEqual(Object obj1,
@@ -130,7 +130,7 @@ public class Validator {
*
* @param obj1 要比较的对象1
* @param obj2 要比较的对象2
* @param message 错误信息
* @param message 提示信息
* @param exceptionType 异常类型
*/
protected static void throwIfNotEqual(Object obj1,
@@ -145,7 +145,7 @@ public class Validator {
*
* @param str1 要比较的字符串1
* @param str2 要比较的字符串2
* @param message 错误信息
* @param message 提示信息
* @param exceptionType 异常类型
*/
protected static void throwIfEqualIgnoreCase(CharSequence str1,
@@ -160,7 +160,7 @@ public class Validator {
*
* @param str1 要比较的字符串1
* @param str2 要比较的字符串2
* @param message 错误信息
* @param message 提示信息
* @param exceptionType 异常类型
*/
protected static void throwIfNotEqualIgnoreCase(CharSequence str1,
@@ -174,7 +174,7 @@ public class Validator {
* 如果条件成立,抛出异常
*
* @param condition 条件
* @param message 错误信息
* @param message 提示信息
* @param exceptionType 异常类型
*/
protected static void throwIf(boolean condition, String message, Class<? extends RuntimeException> exceptionType) {

View File

@@ -30,7 +30,7 @@ import static java.lang.annotation.ElementType.*;
* 枚举校验注解
*
* <p>
* {@code @EnumValue(value = XxxEnum.class, message = "参数值非法")} <br />
* {@code @EnumValue(value = XxxEnum.class, message = "参数值无效")} <br />
* {@code @EnumValue(enumValues = {"F", "M"} ,message = "性别只允许为F或M")}
* </p>
*
@@ -70,7 +70,7 @@ public @interface EnumValue {
*
* @return 提示消息
*/
String message() default "参数值非法";
String message() default "参数值无效";
/**
* 分组

View File

@@ -0,0 +1,66 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.core.validation.constraints;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
/**
* 手机号校验注解
*
* <p>
* 校验中国大陆手机号码
* {@code @Mobile(message = "手机号格式不正确")} <br />
* </p>
*
* @author Charles7c
* @since 2.10.0
*/
@Documented
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
/**
* 提示消息
*
* @return 提示消息
*/
String message() default "手机号格式不正确";
/**
* 分组
*
* @return 分组
*/
Class<?>[] groups() default {};
/**
* 负载
*
* @return 负载
*/
Class<? extends Payload>[] payload() default {};
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.core.validation.constraints;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.PhoneUtil;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
/**
* 手机号校验注解校验器
*
* @author Charles7c
* @since 2.10.0
*/
public class MobileValidator implements ConstraintValidator<Mobile, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (CharSequenceUtil.isBlank(value)) {
return true;
}
return PhoneUtil.isMobile(value);
}
}

View File

@@ -67,7 +67,7 @@ public class SqlInjectionUtils {
* 检查参数是否存在 SQL 注入
*
* @param value 检查参数
* @return true非法false合法
* @return true存在false不存在
*/
public static boolean check(String value) {
return check(value, null);
@@ -78,7 +78,7 @@ public class SqlInjectionUtils {
*
* @param value 检查参数
* @param customKeyword 自定义关键字
* @return true非法false合法
* @return true存在false不存在
*/
public static boolean check(String value, String customKeyword) {
if (CharSequenceUtil.isBlank(value)) {
@@ -114,7 +114,7 @@ public class SqlInjectionUtils {
*
* @param value 检查参数
* @param keywords 关键字列表
* @return true非法false合法
* @return true存在false不存在
*/
private static boolean checkKeyword(String value, String[] keywords) {
for (String keyword : keywords) {

View File

@@ -92,7 +92,7 @@ public class QueryWrapperHelper {
if (sort != null && sort.isSorted()) {
for (Sort.Order order : sort) {
String field = CharSequenceUtil.toUnderlineCase(order.getProperty());
ValidationUtils.throwIf(SqlInjectionUtils.check(field), "排序字段包含非法字符");
ValidationUtils.throwIf(SqlInjectionUtils.check(field), "排序字段包含无效字符");
queryWrapper.orderBy(field, order.isAscending());
}
}

View File

@@ -104,7 +104,7 @@ public class QueryWrapperHelper {
if (sort != null && sort.isSorted()) {
for (Sort.Order order : sort) {
String field = CharSequenceUtil.toUnderlineCase(order.getProperty());
ValidationUtils.throwIf(SqlInjectionUtils.check(field), "排序字段包含非法字符");
ValidationUtils.throwIf(SqlInjectionUtils.check(field), "排序字段包含无效字符");
queryWrapper.orderBy(true, order.isAscending(), field);
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.12</version>
<version>3.3.9</version>
<relativePath/>
</parent>
@@ -43,61 +43,101 @@
<properties>
<!-- 项目版本号 -->
<revision>2.9.0</revision>
<snail-job.version>1.2.0</snail-job.version>
<sa-token.version>1.39.0</sa-token.version>
<revision>2.10.0</revision>
<spring-cloud.version>2023.0.5</spring-cloud.version>
<redisson.version>3.45.0</redisson.version>
<jetcache.version>2.7.7</jetcache.version>
<cosid.version>2.11.0</cosid.version>
<sa-token.version>1.40.0</sa-token.version>
<just-auth.version>1.16.7</just-auth.version>
<mybatis-plus.version>3.5.8</mybatis-plus.version>
<mybatis-flex.version>1.10.3</mybatis-flex.version>
<mybatis-flex.version>1.10.8</mybatis-flex.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
<p6spy.version>3.9.1</p6spy.version>
<jetcache.version>2.7.7</jetcache.version>
<redisson.version>3.41.0</redisson.version>
<cosid.version>2.10.1</cosid.version>
<sms4j.version>3.3.3</sms4j.version>
<aj-captcha.version>1.3.0</aj-captcha.version>
<snail-job.version>1.4.0</snail-job.version>
<sms4j.version>3.3.4</sms4j.version>
<aj-captcha.version>1.4.0</aj-captcha.version>
<easy-captcha.version>1.6.2</easy-captcha.version>
<nashorn.version>15.6</nashorn.version>
<easy-excel.version>3.3.4</easy-excel.version>
<nashorn.version>15.5</nashorn.version>
<x-file-storage.version>2.2.1</x-file-storage.version>
<aws-s3.version>1.12.780</aws-s3.version>
<graceful-response.version>5.0.5-boot3</graceful-response.version>
<aws-s3.version>1.12.782</aws-s3.version>
<s3.version>2.30.35</s3.version>
<s3-crt.version>0.36.1</s3-crt.version>
<thumbnails.version>0.4.20</thumbnails.version>
<crane4j.version>2.9.0</crane4j.version>
<graceful-response.version>5.0.5-boot3</graceful-response.version>
<knife4j.version>4.5.0</knife4j.version>
<tlog.version>1.5.2</tlog.version>
<snakeyaml.version>2.3</snakeyaml.version>
<okhttp.version>4.12.0</okhttp.version>
<ttl.version>2.14.5</ttl.version>
<ip2region.version>3.2.12</ip2region.version>
<hutool.version>5.8.34</hutool.version>
<!--对象存储版本-->
<s3.version>2.29.23</s3.version>
<s3-crt.version>0.33.5</s3-crt.version>
<!--缩略图处理版本-->
<thumbnails.version>0.4.20</thumbnails.version>
<ip2region.version>3.3.6</ip2region.version>
<hutool.version>5.8.36</hutool.version>
<snakeyaml.version>2.4</snakeyaml.version>
<!-- Maven Plugin Versions -->
<flatten.version>1.6.0</flatten.version>
<spotless.version>2.43.0</spotless.version>
<flatten.version>1.7.0</flatten.version>
<spotless.version>2.44.3</spotless.version>
<sonar.version>3.11.0.3922</sonar.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- SnailJob灵活可靠和快速的分布式任务重试和分布式任务调度平台 -->
<!-- Spring CloudSpring 团队提供的微服务解决方案 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-starter</artifactId>
<version>${snail-job.version}</version>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Redisson不仅仅是一个 Redis Java 客户端) -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- JetCache一个基于 Java 的缓存系统封装,提供统一的 API 和注解来简化缓存的使用。提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新还提供了 Cache 接口用于手工缓存操作) -->
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-autoconfigure</artifactId>
<version>${jetcache.version}</version>
</dependency>
<!-- JetCache 注解 -->
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-anno</artifactId>
<version>${jetcache.version}</version>
</dependency>
<!-- JetCache Redisson 适配 -->
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-redisson</artifactId>
<version>${jetcache.version}</version>
<exclusions>
<exclusion>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- CosId通用、灵活、高性能的分布式 ID 生成器) -->
<dependency>
<groupId>me.ahoo.cosid</groupId>
<artifactId>cosid-spring-boot-starter</artifactId>
<version>${cosid.version}</version>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-retry-core</artifactId>
<version>${snail-job.version}</version>
<groupId>me.ahoo.cosid</groupId>
<artifactId>cosid-spring-redis</artifactId>
<version>${cosid.version}</version>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-job-core</artifactId>
<version>${snail-job.version}</version>
<groupId>me.ahoo.cosid</groupId>
<artifactId>cosid-jdbc</artifactId>
<version>${cosid.version}</version>
</dependency>
<!-- Sa-Token轻量级 Java 权限认证框架,让鉴权变得简单、优雅) -->
@@ -174,53 +214,21 @@
<version>${p6spy.version}</version>
</dependency>
<!-- JetCache一个基于 Java 的缓存系统封装,提供统一的 API 和注解来简化缓存的使用。提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新还提供了 Cache 接口用于手工缓存操作 -->
<!-- SnailJob灵活可靠和快速的分布式任务重试和分布式任务调度平台 -->
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-autoconfigure</artifactId>
<version>${jetcache.version}</version>
</dependency>
<!-- JetCache 注解 -->
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-anno</artifactId>
<version>${jetcache.version}</version>
</dependency>
<!-- JetCache Redisson 适配 -->
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-redisson</artifactId>
<version>${jetcache.version}</version>
<exclusions>
<exclusion>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Redisson不仅仅是一个 Redis Java 客户端) -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- CosId通用、灵活、高性能的分布式 ID 生成器) -->
<dependency>
<groupId>me.ahoo.cosid</groupId>
<artifactId>cosid-spring-boot-starter</artifactId>
<version>${cosid.version}</version>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-starter</artifactId>
<version>${snail-job.version}</version>
</dependency>
<dependency>
<groupId>me.ahoo.cosid</groupId>
<artifactId>cosid-spring-redis</artifactId>
<version>${cosid.version}</version>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-retry-core</artifactId>
<version>${snail-job.version}</version>
</dependency>
<dependency>
<groupId>me.ahoo.cosid</groupId>
<artifactId>cosid-jdbc</artifactId>
<version>${cosid.version}</version>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-job-core</artifactId>
<version>${snail-job.version}</version>
</dependency>
<!-- SMS4J短信聚合框架轻松集成多家短信服务解决接入多个短信 SDK 的繁琐流程) -->
@@ -258,6 +266,20 @@
<version>${easy-excel.version}</version>
</dependency>
<!-- X File Storage一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS...等其它兼容 S3 协议的存储平台) -->
<dependency>
<groupId>org.dromara.x-file-storage</groupId>
<artifactId>x-file-storage-spring</artifactId>
<version>${x-file-storage.version}</version>
</dependency>
<!-- Amazon S3Amazon Simple Storage Service亚马逊简单存储服务通用存储协议 S3兼容主流云厂商对象存储 -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>${aws-s3.version}</version>
</dependency>
<!-- S3 for Java 2.x -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
@@ -265,7 +287,7 @@
<version>${s3.version}</version>
</dependency>
<!-- 使用AWS基于 CRT 的 S3 客户端 -->
<!-- 使用 AWS 基于 CRT 的 S3 客户端 -->
<dependency>
<groupId>software.amazon.awssdk.crt</groupId>
<artifactId>aws-crt</artifactId>
@@ -279,7 +301,7 @@
<version>${s3.version}</version>
</dependency>
<!--图片处理工具-主要用做图片缩略处理-->
<!-- Thumbnailator缩略图生成库 -->
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
@@ -537,10 +559,37 @@
<version>${revision}</version>
</dependency>
<!-- Web 模块 -->
<!-- 链路追踪模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-web</artifactId>
<artifactId>continew-starter-trace</artifactId>
<version>${revision}</version>
</dependency>
<!-- 幂等模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-idempotent</artifactId>
<version>${revision}</version>
</dependency>
<!-- 限流模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-ratelimiter</artifactId>
<version>${revision}</version>
</dependency>
<!-- 安全模块 - XSS 过滤 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-security-xss</artifactId>
</dependency>
<!-- 安全模块 - 敏感词 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-security-sensitivewords</artifactId>
<version>${revision}</version>
</dependency>
@@ -565,17 +614,10 @@
<version>${revision}</version>
</dependency>
<!-- 安全模块 - 限流 -->
<!-- Web 模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-security-limiter</artifactId>
<version>${revision}</version>
</dependency>
<!-- 安全模块 - 敏感词 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-security-sensitivewords</artifactId>
<artifactId>continew-starter-web</artifactId>
<version>${revision}</version>
</dependency>

View File

@@ -39,5 +39,5 @@ public @interface CrudRequestMapping {
/**
* API 列表
*/
Api[] api() default {Api.PAGE, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE, Api.EXPORT};
Api[] api() default {Api.PAGE, Api.GET, Api.CREATE, Api.UPDATE, Api.DELETE, Api.EXPORT};
}

View File

@@ -104,27 +104,27 @@ public abstract class AbstractBaseController<S extends BaseService<L, D, Q, C>,
* @param id ID
* @return 详情信息
*/
@CrudApi(Api.DETAIL)
@CrudApi(Api.GET)
@Operation(summary = "查询详情", description = "查询详情")
@Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH)
@ResponseBody
@GetMapping("/{id}")
public D detail(@PathVariable("id") Long id) {
public D get(@PathVariable("id") Long id) {
return baseService.get(id);
}
/**
* 新增
* 创建
*
* @param req 创建参数
* @return ID
*/
@CrudApi(Api.ADD)
@Operation(summary = "新增数据", description = "新增数据")
@CrudApi(Api.CREATE)
@Operation(summary = "创建数据", description = "创建数据")
@ResponseBody
@PostMapping
public BaseIdResp<Long> add(@Validated(CrudValidationGroup.Add.class) @RequestBody C req) {
return new BaseIdResp<>(baseService.add(req));
public BaseIdResp<Long> create(@Validated(CrudValidationGroup.Create.class) @RequestBody C req) {
return new BaseIdResp<>(baseService.create(req));
}
/**

View File

@@ -42,12 +42,12 @@ public enum Api {
/**
* 详情
*/
DETAIL,
GET,
/**
* 新增
* 创建
*/
ADD,
CREATE,
/**
* 修改

View File

@@ -56,7 +56,7 @@ public class SortQuery implements Serializable {
if (ArrayUtil.isEmpty(sort)) {
return Sort.unsorted();
}
ValidationUtils.throwIf(sort.length < 2, "排序条件非法");
ValidationUtils.throwIf(sort.length < 2, "排序条件无效");
List<Sort.Order> orders = new ArrayList<>(sort.length);
if (CharSequenceUtil.contains(sort[0], StringConstants.COMMA)) {
// e.g "sort=createTime,desc&sort=name,asc"
@@ -83,7 +83,7 @@ public class SortQuery implements Serializable {
* @return 排序条件
*/
private Sort.Order getOrder(String field, String direction) {
ValidationUtils.throwIf(SqlInjectionUtils.check(field), "排序字段包含非法字符");
ValidationUtils.throwIf(SqlInjectionUtils.check(field), "排序字段包含无效字符");
return new Sort.Order(Sort.Direction.valueOf(direction.toUpperCase()), field);
}
}

View File

@@ -88,12 +88,12 @@ public interface BaseService<L, D, Q, C> {
List<LabelValueResp> listDict(Q query, SortQuery sortQuery);
/**
* 新增
* 创建
*
* @param req 创建参数
* @return 自增 ID
*/
Long add(C req);
Long create(C req);
/**
* 修改

View File

@@ -27,9 +27,9 @@ import jakarta.validation.groups.Default;
public interface CrudValidationGroup extends Default {
/**
* CRUD 分组校验-新增
* CRUD 分组校验-创建
*/
interface Add extends CrudValidationGroup {}
interface Create extends CrudValidationGroup {}
/**
* CRUD 分组校验-修改

View File

@@ -133,11 +133,11 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
@Override
@Transactional(rollbackFor = Exception.class)
public Long add(C req) {
this.beforeAdd(req);
public Long create(C req) {
this.beforeCreate(req);
T entity = BeanUtil.copyProperties(req, this.entityClass);
mapper.insert(entity);
this.afterAdd(req, entity);
this.afterCreate(req, entity);
return entity.getId();
}
@@ -244,7 +244,7 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
*
* @param req 创建信息
*/
protected void beforeAdd(C req) {
protected void beforeCreate(C req) {
/* 新增前置处理 */
}
@@ -273,7 +273,7 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
* @param req 创建信息
* @param entity 实体信息
*/
protected void afterAdd(C req, T entity) {
protected void afterCreate(C req, T entity) {
/* 新增后置处理 */
}

View File

@@ -176,11 +176,11 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
@Override
@Transactional(rollbackFor = Exception.class)
public Long add(C req) {
this.beforeAdd(req);
public Long create(C req) {
this.beforeCreate(req);
T entity = BeanUtil.copyProperties(req, super.getEntityClass());
baseMapper.insert(entity);
this.afterAdd(req, entity);
this.afterCreate(req, entity);
return entity.getId();
}
@@ -334,7 +334,7 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
*
* @param req 创建信息
*/
protected void beforeAdd(C req) {
protected void beforeCreate(C req) {
/* 新增前置处理 */
}
@@ -363,7 +363,7 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
* @param req 创建信息
* @param entity 实体信息
*/
protected void afterAdd(C req, T entity) {
protected void afterCreate(C req, T entity) {
/* 新增后置处理 */
}

View File

@@ -0,0 +1,27 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-idempotent</artifactId>
<description>ContiNew Starter 幂等模块</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 缓存模块 - Redisson -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-cache-redisson</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.idempotent.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 幂等注解
*
* @author loach
* @author Charles7c
* @since 2.10.0
*/
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/**
* 名称
*/
String name() default "";
/**
* 键(支持 Spring EL 表达式)
*/
String key() default "";
/**
* 超时时间
*/
int timeout() default 1000;
/**
* 时间单位(默认:毫秒)
*/
TimeUnit unit() default TimeUnit.MILLISECONDS;
/**
* 提示信息
*/
String message() default "请勿重复操作";
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.idempotent.aop;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.util.expression.ExpressionUtils;
import top.continew.starter.idempotent.annotation.Idempotent;
import top.continew.starter.idempotent.autoconfigure.IdempotentProperties;
import top.continew.starter.idempotent.exception.IdempotentException;
import top.continew.starter.idempotent.generator.IdempotentNameGenerator;
import java.lang.reflect.Method;
import java.time.Duration;
/**
* 幂等切面
*
* @author loach
* @author Charles7c
* @since 2.10.0
*/
@Aspect
public class IdempotentAspect {
private final IdempotentProperties properties;
private final IdempotentNameGenerator nameGenerator;
public IdempotentAspect(IdempotentProperties properties, IdempotentNameGenerator nameGenerator) {
this.properties = properties;
this.nameGenerator = nameGenerator;
}
/**
* 幂等处理
*
* @param joinPoint 切点
* @param idempotent 幂等注解
* @return 目标方法的执行结果
* @throws Throwable /
*/
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
String cacheKey = this.getCacheKey(joinPoint, idempotent);
// 如果键已存在,则抛出异常
if (!RedisUtils.setIfAbsent(cacheKey, Duration.ofMillis(idempotent.unit().toMillis(idempotent.timeout())))) {
throw new IdempotentException(idempotent.message());
}
// 执行目标方法
try {
return joinPoint.proceed();
} catch (Throwable e) {
// 删除键
RedisUtils.delete(cacheKey);
throw e;
}
}
/**
* 获取缓存 Key
*
* @param joinPoint 切点
* @param idempotent 幂等注解
* @return 缓存 Key
*/
private String getCacheKey(ProceedingJoinPoint joinPoint, Idempotent idempotent) {
Object target = joinPoint.getTarget();
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
Object[] args = joinPoint.getArgs();
// 获取名称
String name = idempotent.name();
if (CharSequenceUtil.isBlank(name)) {
name = nameGenerator.generate(target, method, args);
}
// 解析 Key
String key = idempotent.key();
if (CharSequenceUtil.isNotBlank(key)) {
Object eval = ExpressionUtils.eval(key, target, method, args);
if (ObjectUtil.isNull(eval)) {
throw new IdempotentException("幂等 Key 解析错误");
}
key = Convert.toStr(eval);
}
return RedisUtils.formatKey(properties.getKeyPrefix(), name, key);
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.idempotent.autoconfigure;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ResolvableType;
import top.continew.starter.cache.redisson.autoconfigure.RedissonAutoConfiguration;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.idempotent.aop.IdempotentAspect;
import top.continew.starter.idempotent.generator.IdempotentNameGenerator;
/**
* 幂等自动配置
*
* @author loach
* @author Charles7c
* @since 2.10.0
*/
@AutoConfiguration(after = RedissonAutoConfiguration.class)
@EnableConfigurationProperties(IdempotentProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.IDEMPOTENT, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
public class IdempotentAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(IdempotentAutoConfiguration.class);
/**
* 幂等切面
*/
@Bean
public IdempotentAspect idempotentAspect(IdempotentProperties properties,
IdempotentNameGenerator idempotentNameGenerator) {
return new IdempotentAspect(properties, idempotentNameGenerator);
}
/**
* 幂等名称生成器
*/
@Bean
@ConditionalOnMissingBean
public IdempotentNameGenerator idempotentNameGenerator() {
if (log.isErrorEnabled()) {
log.error("Consider defining a bean of type '{}' in your configuration.", ResolvableType
.forClass(IdempotentNameGenerator.class));
}
throw new NoSuchBeanDefinitionException(IdempotentNameGenerator.class);
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Idempotent' completed initialization.");
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.idempotent.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
/**
* 幂等配置属性
*
* @author loach
* @author Charles7c
* @since 2.10.0
*/
@ConfigurationProperties(PropertiesConstants.IDEMPOTENT)
public class IdempotentProperties {
/**
* Key 前缀
*/
private String keyPrefix = "Idempotent";
public String getKeyPrefix() {
return keyPrefix;
}
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.idempotent.exception;
import top.continew.starter.core.exception.BaseException;
/**
* 幂等异常
*
* @author Charles7c
* @since 2.10.0
*/
public class IdempotentException extends BaseException {
public IdempotentException(String message) {
super(message);
}
public IdempotentException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.idempotent.generator;
import java.lang.reflect.Method;
/**
* 幂等名称生成器
*
* @author loach
* @author Charles7c
* @since 2.10.0
*/
public interface IdempotentNameGenerator {
/**
* 生成幂等名称
*
* @param target 目标实例
* @param method 目标方法
* @param args 方法参数
* @return 幂等名称
*/
String generate(Object target, Method method, Object... args);
}

View File

@@ -0,0 +1 @@
top.continew.starter.idempotent.autoconfigure.IdempotentAutoConfiguration

View File

@@ -0,0 +1,7 @@
{
"top.continew.starter.idempotent.annotation.Idempotent@key":{
"method":{
"parameters": true
}
}
}

View File

@@ -22,13 +22,14 @@ import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import top.continew.starter.log.handler.LogHandler;
import top.continew.starter.log.http.servlet.RecordableServletHttpRequest;
import top.continew.starter.log.http.servlet.RecordableServletHttpResponse;
import top.continew.starter.log.model.AccessLogContext;
import top.continew.starter.log.model.LogProperties;
import java.time.Duration;
import java.time.Instant;
/**
@@ -41,11 +42,12 @@ import java.time.Instant;
@Aspect
public class AccessLogAspect {
private static final Logger log = LoggerFactory.getLogger(AccessLogAspect.class);
private final LogProperties logProperties;
private final LogHandler logHandler;
public AccessLogAspect(LogProperties logProperties) {
public AccessLogAspect(LogProperties logProperties, LogHandler logHandler) {
this.logProperties = logProperties;
this.logHandler = logHandler;
}
/**
@@ -108,19 +110,19 @@ public class AccessLogAspect {
HttpServletRequest request = attributes.getRequest();
HttpServletResponse response = attributes.getResponse();
try {
// 打印请求日志
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
log.info("[{}] {}", request.getMethod(), request.getRequestURI());
}
// 开始访问日志记录
logHandler.accessLogStart(AccessLogContext.builder()
.startTime(startTime)
.request(new RecordableServletHttpRequest(request))
.properties(logProperties)
.build());
return joinPoint.proceed();
} finally {
Instant endTime = Instant.now();
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
Duration timeTaken = Duration.between(startTime, endTime);
log.info("[{}] {} {} {}ms", request.getMethod(), request.getRequestURI(), response != null
? response.getStatus()
: "N/A", timeTaken.toMillis());
}
logHandler.accessLogFinish(AccessLogContext.builder()
.endTime(endTime)
.response(new RecordableServletHttpResponse(response, response.getStatus()))
.build());
}
}
}

View File

@@ -32,7 +32,7 @@ import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import top.continew.starter.log.annotation.Log;
import top.continew.starter.log.dao.LogDao;
import top.continew.starter.log.LogHandler;
import top.continew.starter.log.handler.LogHandler;
import top.continew.starter.log.model.LogProperties;
import top.continew.starter.log.model.LogRecord;
import top.continew.starter.web.util.SpringWebUtils;

View File

@@ -29,9 +29,9 @@ import top.continew.starter.log.aspect.AccessLogAspect;
import top.continew.starter.log.aspect.LogAspect;
import top.continew.starter.log.dao.LogDao;
import top.continew.starter.log.dao.impl.DefaultLogDaoImpl;
import top.continew.starter.log.filter.LogFilter;
import top.continew.starter.log.handler.AopLogHandler;
import top.continew.starter.log.LogFilter;
import top.continew.starter.log.LogHandler;
import top.continew.starter.log.handler.LogHandler;
import top.continew.starter.log.model.LogProperties;
/**
@@ -49,9 +49,11 @@ public class LogAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(LogAutoConfiguration.class);
private final LogProperties logProperties;
private final LogHandler logHandler;
public LogAutoConfiguration(LogProperties logProperties) {
public LogAutoConfiguration(LogProperties logProperties, LogHandler logHandler) {
this.logProperties = logProperties;
this.logHandler = logHandler;
}
/**
@@ -66,13 +68,12 @@ public class LogAutoConfiguration {
/**
* 日志切面
*
* @param logHandler 日志处理器
* @param logDao 日志持久层接口
* @param logDao 日志持久层接口
* @return {@link LogAspect }
*/
@Bean
@ConditionalOnMissingBean
public LogAspect logAspect(LogHandler logHandler, LogDao logDao) {
public LogAspect logAspect(LogDao logDao) {
return new LogAspect(logProperties, logHandler, logDao);
}
@@ -84,7 +85,7 @@ public class LogAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AccessLogAspect accessLogAspect() {
return new AccessLogAspect(logProperties);
return new AccessLogAspect(logProperties, logHandler);
}
/**

View File

@@ -16,8 +16,6 @@
package top.continew.starter.log.handler;
import top.continew.starter.log.AbstractLogHandler;
/**
* 日志处理器-AOP 版实现
*

View File

@@ -18,5 +18,11 @@
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
</dependency>
<!-- TTL线程间传递 ThreadLocal异步执行时上下文传递的解决方案 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.log;
package top.continew.starter.log.filter;
import cn.hutool.extra.spring.SpringUtil;
import jakarta.servlet.FilterChain;
@@ -25,15 +25,13 @@ import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.core.Ordered;
import org.springframework.lang.NonNull;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import top.continew.starter.web.util.RepeatReadRequestWrapper;
import top.continew.starter.web.util.RepeatReadResponseWrapper;
import top.continew.starter.log.model.LogProperties;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
/**
* 日志过滤器
@@ -44,6 +42,7 @@ import java.util.Objects;
* @author Venil NoronhaSpring Boot Actuator
* @author Madhura BhaveSpring Boot Actuator
* @author Charles7c
* @author echo
* @since 1.1.0
*/
public class LogFilter extends OncePerRequestFilter implements Ordered {
@@ -67,20 +66,24 @@ public class LogFilter extends OncePerRequestFilter implements Ordered {
filterChain.doFilter(request, response);
return;
}
boolean isMatch = logProperties.isMatch(request.getRequestURI());
// 包装输入流可重复读取
if (!isMatch && this.isRequestWrapper(request)) {
request = new ContentCachingRequestWrapper(request);
}
// 包装输出流可重复读取
boolean isResponseWrapper = !isMatch && this.isResponseWrapper(response);
if (isResponseWrapper) {
response = new ContentCachingResponseWrapper(response);
}
filterChain.doFilter(request, response);
// 更新响应不操作这一步会导致接口响应空白
if (isResponseWrapper) {
this.updateResponse(response);
boolean isExcludeUri = logProperties.isMatch(request.getRequestURI());
// 处理可重复读取的请求
HttpServletRequest requestWrapper = (isExcludeUri || !this.isRequestWrapper(request))
? request
: new RepeatReadRequestWrapper(request);
// 处理可重复读取的响应
HttpServletResponse responseWrapper = (isExcludeUri || !this.isResponseWrapper(response))
? response
: new RepeatReadResponseWrapper(response);
filterChain.doFilter(requestWrapper, responseWrapper);
// 如果响应被包装了复制缓存数据到原始响应
if (responseWrapper instanceof RepeatReadResponseWrapper wrappedResponse) {
wrappedResponse.copyBodyToResponse();
}
}
@@ -121,7 +124,7 @@ public class LogFilter extends OncePerRequestFilter implements Ordered {
* @return truefalse
*/
private boolean isRequestWrapper(HttpServletRequest request) {
return !(request instanceof ContentCachingRequestWrapper);
return !(request instanceof RepeatReadRequestWrapper);
}
/**
@@ -131,18 +134,6 @@ public class LogFilter extends OncePerRequestFilter implements Ordered {
* @return truefalse
*/
private boolean isResponseWrapper(HttpServletResponse response) {
return !(response instanceof ContentCachingResponseWrapper);
}
/**
* 更新响应
*
* @param response 响应对象
* @throws IOException /
*/
private void updateResponse(HttpServletResponse response) throws IOException {
ContentCachingResponseWrapper responseWrapper = WebUtils
.getNativeResponse(response, ContentCachingResponseWrapper.class);
Objects.requireNonNull(responseWrapper).copyBodyToResponse();
return !(response instanceof RepeatReadResponseWrapper);
}
}

View File

@@ -14,21 +14,31 @@
* limitations under the License.
*/
package top.continew.starter.log;
package top.continew.starter.log.handler;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.starter.log.annotation.Log;
import top.continew.starter.log.enums.Include;
import top.continew.starter.log.http.RecordableHttpRequest;
import top.continew.starter.log.http.RecordableHttpResponse;
import top.continew.starter.log.http.servlet.RecordableServletHttpRequest;
import top.continew.starter.log.http.servlet.RecordableServletHttpResponse;
import top.continew.starter.log.model.AccessLogContext;
import top.continew.starter.log.model.AccessLogProperties;
import top.continew.starter.log.model.LogRecord;
import top.continew.starter.log.util.AccessLogUtils;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
@@ -41,6 +51,9 @@ import java.util.Set;
*/
public abstract class AbstractLogHandler implements LogHandler {
private static final Logger log = LoggerFactory.getLogger(AbstractLogHandler.class);
private final TransmittableThreadLocal<AccessLogContext> logContextThread = new TransmittableThreadLocal<>();
@Override
public LogRecord.Started start(Instant startTime, HttpServletRequest request) {
return LogRecord.start(startTime, new RecordableServletHttpRequest(request));
@@ -156,4 +169,37 @@ public abstract class AbstractLogHandler implements LogHandler {
includes.removeAll(Set.of(excludeArr));
}
}
@Override
public void accessLogStart(AccessLogContext accessLogContext) {
AccessLogProperties properties = accessLogContext.getProperties().getAccessLog();
// 是否需要打印 规则: 是否打印开关 放行路径
if (!properties.isEnabled() || accessLogContext.getProperties()
.isMatch(accessLogContext.getRequest().getPath())) {
return;
}
// 构建上下文
logContextThread.set(accessLogContext);
RecordableHttpRequest request = accessLogContext.getRequest();
String path = request.getPath();
String param = AccessLogUtils.getParam(request, properties);
log.info(param != null ? "[Start] [{}] {} param: {}" : "[Start] [{}] {}", request.getMethod(), path, param);
}
@Override
public void accessLogFinish(AccessLogContext accessLogContext) {
AccessLogContext logContext = logContextThread.get();
if (ObjectUtil.isEmpty(logContext)) {
return;
}
try {
RecordableHttpRequest request = logContext.getRequest();
RecordableHttpResponse response = accessLogContext.getResponse();
Duration timeTaken = Duration.between(logContext.getStartTime(), accessLogContext.getEndTime());
log.info("[End] [{}] {} {} {}ms", request.getMethod(), request.getPath(), response.getStatus(), timeTaken
.toMillis());
} finally {
logContextThread.remove();
}
}
}

View File

@@ -14,11 +14,12 @@
* limitations under the License.
*/
package top.continew.starter.log;
package top.continew.starter.log.handler;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import top.continew.starter.log.enums.Include;
import top.continew.starter.log.model.AccessLogContext;
import top.continew.starter.log.model.LogRecord;
import java.lang.reflect.Method;
@@ -29,6 +30,7 @@ import java.util.Set;
* 日志处理器
*
* @author Charles7c
* @author echo
* @since 2.8.0
*/
public interface LogHandler {
@@ -97,4 +99,20 @@ public interface LogHandler {
* @return 日志包含信息
*/
Set<Include> getIncludes(Set<Include> includes, Method targetMethod, Class<?> targetClass);
/**
* 开始访问日志记录
*
* @param accessLogContext 访问日志上下文
* @since 2.10.0
*/
void accessLogStart(AccessLogContext accessLogContext);
/**
* 结束访问日志记录
*
* @param accessLogContext 访问日志上下文
* @since 2.10.0
*/
void accessLogFinish(AccessLogContext accessLogContext);
}

View File

@@ -25,6 +25,7 @@ import java.util.Map;
* @author Andy WilkinsonSpring Boot Actuator
* @author Phillip WebbSpring Boot Actuator
* @author Charles7c
* @author echo
* @see RecordableHttpResponse
* @since 1.1.0
*/
@@ -45,11 +46,13 @@ public interface RecordableHttpRequest {
URI getUrl();
/**
* 获取 IP
* 获取路径
* <p>/foo/bar</p>
*
* @return IP
* @return 路径
* @since 2.10.0
*/
String getIp();
String getPath();
/**
* 获取请求头
@@ -71,4 +74,11 @@ public interface RecordableHttpRequest {
* @return 请求参数
*/
Map<String, Object> getParam();
/**
* 获取 IP
*
* @return IP
*/
String getIp();
}

View File

@@ -17,15 +17,13 @@
package top.continew.starter.log.http.servlet;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.WebUtils;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.log.http.RecordableHttpRequest;
import top.continew.starter.web.util.RepeatReadRequestWrapper;
import java.net.URI;
import java.net.URISyntaxException;
@@ -38,6 +36,8 @@ import java.util.Map;
*
* @author Andy WilkinsonSpring Boot Actuator
* @author Charles7c
* @author echo
* @since 1.1.0
*/
public final class RecordableServletHttpRequest implements RecordableHttpRequest {
@@ -69,8 +69,8 @@ public final class RecordableServletHttpRequest implements RecordableHttpRequest
}
@Override
public String getIp() {
return JakartaServletUtil.getClientIP(request);
public String getPath() {
return request.getRequestURI();
}
@Override
@@ -80,9 +80,8 @@ public final class RecordableServletHttpRequest implements RecordableHttpRequest
@Override
public String getBody() {
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (null != wrapper) {
String body = StrUtil.utf8Str(wrapper.getContentAsByteArray());
if (request instanceof RepeatReadRequestWrapper wrapper && !wrapper.isMultipartContent(request)) {
String body = JakartaServletUtil.getBody(request);
return JSONUtil.isTypeJSON(body) ? body : null;
}
return null;
@@ -93,7 +92,12 @@ public final class RecordableServletHttpRequest implements RecordableHttpRequest
String body = this.getBody();
return CharSequenceUtil.isNotBlank(body) && JSONUtil.isTypeJSON(body)
? JSONUtil.toBean(body, Map.class)
: Collections.unmodifiableMap(request.getParameterMap());
: Collections.unmodifiableMap(JakartaServletUtil.getParamMap(request));
}
@Override
public String getIp() {
return JakartaServletUtil.getClientIP(request);
}
private StringBuilder appendQueryString(String queryString) {

View File

@@ -17,13 +17,11 @@
package top.continew.starter.log.http.servlet;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import top.continew.starter.log.http.RecordableHttpResponse;
import top.continew.starter.web.util.ServletUtils;
import top.continew.starter.web.util.RepeatReadResponseWrapper;
import top.continew.starter.log.http.RecordableHttpResponse;
import java.util.Map;
@@ -32,6 +30,8 @@ import java.util.Map;
*
* @author Andy WilkinsonSpring Boot Actuator
* @author Charles7c
* @author echo
* @since 1.1.0
*/
public final class RecordableServletHttpResponse implements RecordableHttpResponse {
@@ -56,10 +56,8 @@ public final class RecordableServletHttpResponse implements RecordableHttpRespon
@Override
public String getBody() {
ContentCachingResponseWrapper wrapper = WebUtils
.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (null != wrapper) {
String body = StrUtil.utf8Str(wrapper.getContentAsByteArray());
if (response instanceof RepeatReadResponseWrapper wrapper && !wrapper.isStreamingResponse()) {
String body = wrapper.getResponseContent();
return JSONUtil.isTypeJSON(body) ? body : null;
}
return null;

View File

@@ -0,0 +1,136 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.log.model;
import top.continew.starter.log.http.RecordableHttpRequest;
import top.continew.starter.log.http.RecordableHttpResponse;
import java.time.Instant;
/**
* 访问日志上下文
*
* @author echo
* @since 2.10.0
*/
public class AccessLogContext {
/**
* 开始时间
*/
private final Instant startTime;
/**
* 结束时间
*/
private Instant endTime;
/**
* 请求信息
*/
private final RecordableHttpRequest request;
/**
* 响应信息
*/
private final RecordableHttpResponse response;
/**
* 配置信息
*/
private final LogProperties properties;
private AccessLogContext(Builder builder) {
this.startTime = builder.startTime;
this.endTime = builder.endTime;
this.request = builder.request;
this.response = builder.response;
this.properties = builder.properties;
}
public Instant getStartTime() {
return startTime;
}
public Instant getEndTime() {
return endTime;
}
public RecordableHttpRequest getRequest() {
return request;
}
public RecordableHttpResponse getResponse() {
return response;
}
public LogProperties getProperties() {
return properties;
}
public void setEndTime(Instant endTime) {
this.endTime = endTime;
}
public static Builder builder() {
return new Builder();
}
/**
* 访问日志上下文构建者
*/
public static class Builder {
private Instant startTime;
private Instant endTime;
private RecordableHttpRequest request;
private RecordableHttpResponse response;
private LogProperties properties;
private Builder() {
}
public Builder startTime(Instant startTime) {
this.startTime = startTime;
return this;
}
public Builder endTime(Instant endTime) {
this.endTime = endTime;
return this;
}
public Builder request(RecordableHttpRequest request) {
this.request = request;
return this;
}
public Builder response(RecordableHttpResponse response) {
this.response = response;
return this;
}
public Builder properties(LogProperties properties) {
this.properties = properties;
return this;
}
public AccessLogContext build() {
return new AccessLogContext(this);
}
}
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.log.model;
import java.util.ArrayList;
import java.util.List;
/**
* 访问日志配置属性
*
* @author echo
* @author Charles7c
* @since 2.10.0
*/
public class AccessLogProperties {
/**
* 是否打印访问日志(类似于 Nginx access log
* <p>
* 不记录请求日志也支持开启打印访问日志
* </p>
*/
private boolean enabled = false;
/**
* 是否打印请求参数body/query/form
* <p>开启后,访问日志会打印请求参数</p>
*/
private boolean isPrintRequestParam = false;
/**
* 是否自动截断超长参数值(如 base64、大文本
* <p>开启后,超过指定长度的参数值将会自动截断处理</p>
*/
private boolean longParamTruncate = false;
/**
* 超长参数检测阈值(单位:字符)
* <p>当参数值长度超过此值时,触发截断规则</p>
* <p>默认2000仅在 {@link #longParamTruncate} 启用时生效</p>
*/
private int longParamThreshold = 2000;
/**
* 超长参数最大保留长度(单位:字符)
* <p>当参数超过 {@link #longParamThreshold} 时,强制截断到此长度</p>
* <p>默认50仅在 {@link #longParamTruncate} 启用时生效</p>
*/
private int longParamMaxLength = 50;
/**
* 截断后追加的后缀符号(如配置 "..." 会让截断内容更直观)
* <p>建议配置 3-5 个非占宽字符,默认为 ...</p>
* <p>仅在 {@link #longParamTruncate} 启用时生效</p>
*/
private String longParamSuffix = "...";
/**
* 是否过滤敏感参数
* <p>开启后会对敏感参数进行过滤,默认不过滤</p>
*/
private boolean isParamSensitive = false;
/**
* 敏感参数字段列表password,token,idCard
* <p>支持精确匹配(区分大小写)</p>
* <p>示例值password,oldPassword</p>
*/
private List<String> sensitiveParams = new ArrayList<>();
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isPrintRequestParam() {
return isPrintRequestParam;
}
public void setPrintRequestParam(boolean printRequestParam) {
isPrintRequestParam = printRequestParam;
}
public boolean isLongParamTruncate() {
return longParamTruncate;
}
public void setLongParamTruncate(boolean longParamTruncate) {
this.longParamTruncate = longParamTruncate;
}
public int getLongParamThreshold() {
return longParamThreshold;
}
public void setLongParamThreshold(int longParamThreshold) {
this.longParamThreshold = longParamThreshold;
}
public int getLongParamMaxLength() {
return longParamMaxLength;
}
public void setLongParamMaxLength(int longParamMaxLength) {
this.longParamMaxLength = longParamMaxLength;
}
public String getLongParamSuffix() {
return longParamSuffix;
}
public void setLongParamSuffix(String longParamSuffix) {
this.longParamSuffix = longParamSuffix;
}
public boolean isParamSensitive() {
return isParamSensitive;
}
public void setParamSensitive(boolean paramSensitive) {
isParamSensitive = paramSensitive;
}
public List<String> getSensitiveParams() {
return sensitiveParams;
}
public void setSensitiveParams(List<String> sensitiveParams) {
this.sensitiveParams = sensitiveParams;
}
}

View File

@@ -17,6 +17,7 @@
package top.continew.starter.log.model;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.log.enums.Include;
import top.continew.starter.web.util.SpringWebUtils;
@@ -40,12 +41,10 @@ public class LogProperties {
private boolean enabled = true;
/**
* 是否打印日志,开启后可打印访问日志(类似于 Nginx access log
* <p>
* 不记录日志也支持开启打印访问日志
* </p>
* 访问日志配置
*/
private Boolean isPrint = false;
@NestedConfigurationProperty
private AccessLogProperties accessLog = new AccessLogProperties();
/**
* 包含信息
@@ -65,14 +64,6 @@ public class LogProperties {
this.enabled = enabled;
}
public Boolean getIsPrint() {
return isPrint;
}
public void setIsPrint(Boolean print) {
isPrint = print;
}
public Set<Include> getIncludes() {
return includes;
}
@@ -89,6 +80,14 @@ public class LogProperties {
this.excludePatterns = excludePatterns;
}
public AccessLogProperties getAccessLog() {
return accessLog;
}
public void setAccessLog(AccessLogProperties accessLog) {
this.accessLog = accessLog;
}
/**
* 是否匹配放行路由
*

View File

@@ -20,9 +20,9 @@ import cn.hutool.core.text.CharSequenceUtil;
import org.springframework.http.HttpHeaders;
import top.continew.starter.core.util.ExceptionUtils;
import top.continew.starter.core.util.IpUtils;
import top.continew.starter.web.util.ServletUtils;
import top.continew.starter.log.enums.Include;
import top.continew.starter.log.http.RecordableHttpRequest;
import top.continew.starter.web.util.ServletUtils;
import java.net.URI;
import java.util.Map;

View File

@@ -0,0 +1,125 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.log.util;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import top.continew.starter.log.http.RecordableHttpRequest;
import top.continew.starter.log.model.AccessLogProperties;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 访问日志工具类
*
* @author echo
* @author Charles7c
* @since 2.10.0
*/
public class AccessLogUtils {
/**
* 获取参数信息
*
* @param request 请求
* @param properties 属性
* @return {@link String }
*/
public static String getParam(RecordableHttpRequest request, AccessLogProperties properties) {
// 是否需要打印请求参数
if (!properties.isPrintRequestParam()) {
return null;
}
// 参数为空返回空
Map<String, Object> params;
try {
params = request.getParam();
} catch (Exception e) {
return null;
}
if (ObjectUtil.isEmpty(params) || params.isEmpty()) {
return null;
}
// 是否需要对特定入参脱敏
if (properties.isParamSensitive()) {
params = filterSensitiveParams(params, properties.getSensitiveParams());
}
// 是否自动截断超长参数值
if (properties.isLongParamTruncate()) {
params = truncateLongParams(params, properties.getLongParamThreshold(), properties
.getLongParamMaxLength(), properties.getLongParamSuffix());
}
return JSONUtil.toJsonStr(params);
}
/**
* 过滤敏感参数
*
* @param params 参数 Map
* @param sensitiveParams 敏感参数列表
* @return 处理后的参数 Map
*/
private static Map<String, Object> filterSensitiveParams(Map<String, Object> params, List<String> sensitiveParams) {
if (params == null || params.isEmpty() || sensitiveParams == null || sensitiveParams.isEmpty()) {
return params;
}
Map<String, Object> filteredParams = new HashMap<>(params);
for (String sensitiveKey : sensitiveParams) {
filteredParams.computeIfPresent(sensitiveKey, (key, value) -> "***");
}
return filteredParams;
}
/**
* 截断超长参数
*
* @param params 参数 Map
* @param threshold 截断阈值(值长度超过该值才截断)
* @param maxLength 最大长度
* @param suffix 后缀(如 "..."
* @return 处理后的参数 Map
*/
private static Map<String, Object> truncateLongParams(Map<String, Object> params,
int threshold,
int maxLength,
String suffix) {
if (params == null || params.isEmpty()) {
return params;
}
Map<String, Object> truncatedParams = new HashMap<>(params);
for (Map.Entry<String, Object> entry : truncatedParams.entrySet()) {
Object value = entry.getValue();
if (value instanceof String strValue) {
if (strValue.length() > threshold) {
entry.setValue(strValue.substring(0, Math.min(strValue.length(), maxLength)) + suffix);
}
}
}
return truncatedParams;
}
private AccessLogUtils() {
}
}

View File

@@ -13,12 +13,6 @@
<description>ContiNew Starter 日志模块 - 基于拦截器实现Spring Boot Actuator HttpTrace 增强版)</description>
<dependencies>
<!-- TTL线程间传递 ThreadLocal异步执行时上下文传递的解决方案 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
<!-- 日志模块 - 核心模块 -->
<dependency>
<groupId>top.continew</groupId>

View File

@@ -30,8 +30,8 @@ import top.continew.starter.log.annotation.ConditionalOnEnabledLog;
import top.continew.starter.log.dao.LogDao;
import top.continew.starter.log.dao.impl.DefaultLogDaoImpl;
import top.continew.starter.log.handler.InterceptorLogHandler;
import top.continew.starter.log.LogFilter;
import top.continew.starter.log.LogHandler;
import top.continew.starter.log.filter.LogFilter;
import top.continew.starter.log.handler.LogHandler;
import top.continew.starter.log.interceptor.LogInterceptor;
import top.continew.starter.log.model.LogProperties;

View File

@@ -16,8 +16,6 @@
package top.continew.starter.log.handler;
import top.continew.starter.log.AbstractLogHandler;
/**
* 日志处理器-拦截器版实现
*

View File

@@ -27,13 +27,15 @@ import org.springframework.lang.NonNull;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import top.continew.starter.log.annotation.Log;
import top.continew.starter.log.http.servlet.RecordableServletHttpRequest;
import top.continew.starter.log.http.servlet.RecordableServletHttpResponse;
import top.continew.starter.log.model.AccessLogContext;
import top.continew.starter.log.model.LogProperties;
import top.continew.starter.log.dao.LogDao;
import top.continew.starter.log.LogHandler;
import top.continew.starter.log.handler.LogHandler;
import top.continew.starter.log.model.LogRecord;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.Instant;
/**
@@ -62,10 +64,11 @@ public class LogInterceptor implements HandlerInterceptor {
@NonNull HttpServletResponse response,
@NonNull Object handler) {
Instant startTime = Instant.now();
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
log.info("[{}] {}", request.getMethod(), request.getRequestURI());
timeTtl.set(startTime);
}
logHandler.accessLogStart(AccessLogContext.builder()
.startTime(startTime)
.request(new RecordableServletHttpRequest(request))
.properties(logProperties)
.build());
// 开始日志记录
if (this.isRequestRecord(handler, request)) {
LogRecord.Started startedLogRecord = logHandler.start(startTime, request);
@@ -81,11 +84,10 @@ public class LogInterceptor implements HandlerInterceptor {
Exception e) {
try {
Instant endTime = Instant.now();
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
Duration timeTaken = Duration.between(timeTtl.get(), endTime);
log.info("[{}] {} {} {}ms", request.getMethod(), request.getRequestURI(), response
.getStatus(), timeTaken.toMillis());
}
logHandler.accessLogFinish(AccessLogContext.builder()
.endTime(endTime)
.response(new RecordableServletHttpResponse(response, response.getStatus()))
.build());
LogRecord.Started startedLogRecord = logTtl.get();
if (null == startedLogRecord) {
return;

View File

@@ -174,7 +174,7 @@ public class MailConfig {
javaMailProperties.put("mail.smtp.auth", true);
javaMailProperties.put("mail.smtp.ssl.enable", this.isSslEnabled());
if (this.isSslEnabled()) {
ValidationUtils.throwIfNull(this.getSslPort(), "邮件配置错误SSL端口不能为空");
ValidationUtils.throwIfNull(this.getSslPort(), "邮件配置不正确SSL端口不能为空");
javaMailProperties.put("mail.smtp.socketFactory.port", this.sslPort);
javaMailProperties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
}

View File

@@ -42,20 +42,20 @@ public interface MailConfigurer {
*/
default void apply(MailConfig mailConfig, JavaMailSenderImpl sender) {
String protocolLowerCase = mailConfig.getProtocol().toLowerCase();
ValidationUtils.throwIfNotEqual(MailConfig.DEFAULT_PROTOCOL, protocolLowerCase, "邮件配置错误:不支持的邮件发送协议: %s"
ValidationUtils.throwIfNotEqual(MailConfig.DEFAULT_PROTOCOL, protocolLowerCase, "邮件配置不正确:不支持的邮件发送协议: %s"
.formatted(mailConfig.getProtocol()));
sender.setProtocol(mailConfig.getProtocol());
ValidationUtils.throwIfBlank(mailConfig.getHost(), "邮件配置错误:服务器地址不能为空");
ValidationUtils.throwIfBlank(mailConfig.getHost(), "邮件配置不正确:服务器地址不能为空");
sender.setHost(mailConfig.getHost());
ValidationUtils.throwIfNull(mailConfig.getPort(), "邮件配置错误:服务器端口不能为空");
ValidationUtils.throwIfNull(mailConfig.getPort(), "邮件配置不正确:服务器端口不能为空");
sender.setPort(mailConfig.getPort());
ValidationUtils.throwIfBlank(mailConfig.getUsername(), "邮件配置错误:用户名不能为空");
ValidationUtils.throwIfBlank(mailConfig.getUsername(), "邮件配置不正确:用户名不能为空");
sender.setUsername(mailConfig.getUsername());
ValidationUtils.throwIfBlank(mailConfig.getPassword(), "邮件配置错误:密码不能为空");
ValidationUtils.throwIfBlank(mailConfig.getPassword(), "邮件配置不正确:密码不能为空");
sender.setPassword(mailConfig.getPassword());
if (mailConfig.getDefaultEncoding() != null) {

View File

@@ -3,19 +3,14 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-security</artifactId>
<artifactId>continew-starter</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-security-limiter</artifactId>
<description>ContiNew Starter 安全模块 - 限流</description>
<artifactId>continew-starter-ratelimiter</artifactId>
<description>ContiNew Starter 限流模块</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Web 模块 -->
<dependency>
<groupId>top.continew</groupId>

View File

@@ -14,9 +14,9 @@
* limitations under the License.
*/
package top.continew.starter.security.limiter.annotation;
package top.continew.starter.ratelimiter.annotation;
import top.continew.starter.security.limiter.enums.LimitType;
import top.continew.starter.ratelimiter.enums.LimitType;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.security.limiter.annotation;
package top.continew.starter.ratelimiter.annotation;
import java.lang.annotation.*;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.security.limiter.core;
package top.continew.starter.ratelimiter.aop;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.text.CharSequenceUtil;
@@ -27,15 +27,15 @@ import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.*;
import org.springframework.stereotype.Component;
import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.expression.ExpressionUtils;
import top.continew.starter.security.limiter.annotation.RateLimiter;
import top.continew.starter.security.limiter.annotation.RateLimiters;
import top.continew.starter.security.limiter.autoconfigure.RateLimiterProperties;
import top.continew.starter.security.limiter.enums.LimitType;
import top.continew.starter.security.limiter.exception.RateLimiterException;
import top.continew.starter.ratelimiter.annotation.RateLimiter;
import top.continew.starter.ratelimiter.annotation.RateLimiters;
import top.continew.starter.ratelimiter.autoconfigure.RateLimiterProperties;
import top.continew.starter.ratelimiter.generator.RateLimiterNameGenerator;
import top.continew.starter.ratelimiter.enums.LimitType;
import top.continew.starter.ratelimiter.exception.RateLimiterException;
import top.continew.starter.web.util.SpringWebUtils;
import java.lang.reflect.Method;
@@ -51,7 +51,6 @@ import java.util.concurrent.ConcurrentHashMap;
* @since 2.2.0
*/
@Aspect
@Component
public class RateLimiterAspect {
private static final ConcurrentHashMap<String, RRateLimiter> RATE_LIMITER_CACHE = new ConcurrentHashMap<>();
@@ -70,14 +69,14 @@ public class RateLimiterAspect {
/**
* 单个限流注解切点
*/
@Pointcut("@annotation(top.continew.starter.security.limiter.annotation.RateLimiter)")
@Pointcut("@annotation(top.continew.starter.ratelimiter.annotation.RateLimiter)")
public void rateLimiterPointCut() {
}
/**
* 多个限流注解切点
*/
@Pointcut("@annotation(top.continew.starter.security.limiter.annotation.RateLimiters)")
@Pointcut("@annotation(top.continew.starter.ratelimiter.annotation.RateLimiters)")
public void rateLimitersPointCut() {
}
@@ -144,23 +143,23 @@ public class RateLimiterAspect {
}
/**
* 获取限流缓存 Key
* 获取缓存 Key
*
* @param joinPoint 切点
* @param rateLimiter 限流注解
* @return 限流缓存 Key
* @return 缓存 Key
*/
private String getCacheKey(JoinPoint joinPoint, RateLimiter rateLimiter) {
Object target = joinPoint.getTarget();
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
Object[] args = joinPoint.getArgs();
// 获取限流名称
// 获取名称
String name = rateLimiter.name();
if (CharSequenceUtil.isBlank(name)) {
name = nameGenerator.generate(target, method, args);
}
// 解析限流 Key
// 解析 Key
String key = rateLimiter.key();
if (CharSequenceUtil.isNotBlank(key)) {
Object eval = ExpressionUtils.eval(key, target, method, args);

View File

@@ -14,9 +14,10 @@
* limitations under the License.
*/
package top.continew.starter.security.limiter.autoconfigure;
package top.continew.starter.ratelimiter.autoconfigure;
import jakarta.annotation.PostConstruct;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -24,10 +25,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import top.continew.starter.cache.redisson.autoconfigure.RedissonAutoConfiguration;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.security.limiter.core.DefaultRateLimiterNameGenerator;
import top.continew.starter.security.limiter.core.RateLimiterNameGenerator;
import top.continew.starter.ratelimiter.aop.RateLimiterAspect;
import top.continew.starter.ratelimiter.generator.DefaultRateLimiterNameGenerator;
import top.continew.starter.ratelimiter.generator.RateLimiterNameGenerator;
/**
* 限流器自动配置
@@ -36,14 +38,23 @@ import top.continew.starter.security.limiter.core.RateLimiterNameGenerator;
* @author Charles7c
* @since 2.2.0
*/
@AutoConfiguration
@AutoConfiguration(after = RedissonAutoConfiguration.class)
@EnableConfigurationProperties(RateLimiterProperties.class)
@ComponentScan({"top.continew.starter.security.limiter.core"})
@ConditionalOnProperty(prefix = PropertiesConstants.SECURITY_LIMITER, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
@ConditionalOnProperty(prefix = PropertiesConstants.RATE_LIMITER, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
public class RateLimiterAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(RateLimiterAutoConfiguration.class);
/**
* 限流器切面
*/
@Bean
public RateLimiterAspect rateLimiterAspect(RateLimiterProperties properties,
RateLimiterNameGenerator rateLimiterNameGenerator,
RedissonClient redissonClient) {
return new RateLimiterAspect(properties, rateLimiterNameGenerator, redissonClient);
}
/**
* 限流器名称生成器
*/
@@ -55,6 +66,6 @@ public class RateLimiterAutoConfiguration {
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Security-RateLimiter' completed initialization.");
log.debug("[ContiNew Starter] - Auto Configuration 'RateLimiter' completed initialization.");
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.security.limiter.autoconfigure;
package top.continew.starter.ratelimiter.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
@@ -25,7 +25,7 @@ import top.continew.starter.core.constant.PropertiesConstants;
* @author KAI
* @since 2.2.0
*/
@ConfigurationProperties(PropertiesConstants.SECURITY_LIMITER)
@ConfigurationProperties(PropertiesConstants.RATE_LIMITER)
public class RateLimiterProperties {
/**

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.security.limiter.enums;
package top.continew.starter.ratelimiter.enums;
/**
* 限流类型

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.security.limiter.exception;
package top.continew.starter.ratelimiter.exception;
import top.continew.starter.core.exception.BaseException;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.security.limiter.core;
package top.continew.starter.ratelimiter.generator;
import cn.hutool.core.util.ClassUtil;
import top.continew.starter.core.constant.StringConstants;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.security.limiter.core;
package top.continew.starter.ratelimiter.generator;
import java.lang.reflect.Method;

View File

@@ -0,0 +1 @@
top.continew.starter.ratelimiter.autoconfigure.RateLimiterAutoConfiguration

View File

@@ -0,0 +1,7 @@
{
"top.continew.starter.ratelimiter.annotation.RateLimiter@key":{
"method":{
"parameters": true
}
}
}

View File

@@ -1 +0,0 @@
top.continew.starter.security.limiter.autoconfigure.RateLimiterAutoConfiguration

View File

@@ -1,7 +0,0 @@
{
"top.continew.starter.security.limiter.annotation.RateLimiter@key":{
"method":{
"parameters": true
}
}
}

View File

@@ -0,0 +1,22 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-security</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-security-xss</artifactId>
<description>ContiNew Starter 安全模块 - XSS 过滤模块</description>
<dependencies>
<!-- Web 模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -14,8 +14,11 @@
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.xss;
package top.continew.starter.security.xss.autoconfigure;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
@@ -23,6 +26,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.security.xss.filter.XssFilter;
/**
* XSS 过滤自动配置
@@ -33,9 +37,11 @@ import top.continew.starter.core.constant.PropertiesConstants;
@AutoConfiguration
@ConditionalOnWebApplication
@EnableConfigurationProperties(XssProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.WEB_XSS, name = PropertiesConstants.ENABLED, havingValue = "true")
@ConditionalOnProperty(prefix = PropertiesConstants.SECURITY_XSS, name = PropertiesConstants.ENABLED, havingValue = "true")
public class XssAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(XssAutoConfiguration.class);
/**
* XSS 过滤器配置
*/
@@ -45,4 +51,9 @@ public class XssAutoConfiguration {
registrationBean.setFilter(new XssFilter(xssProperties));
return registrationBean;
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Security-XSS' completed initialization.");
}
}

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.xss;
package top.continew.starter.security.xss.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.web.enums.XssMode;
import top.continew.starter.security.xss.enums.XssMode;
import java.util.ArrayList;
import java.util.List;
@@ -29,7 +29,7 @@ import java.util.List;
* @author whhya
* @since 2.0.0
*/
@ConfigurationProperties(PropertiesConstants.WEB_XSS)
@ConfigurationProperties(PropertiesConstants.SECURITY_XSS)
public class XssProperties {
/**

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.web.enums;
package top.continew.starter.security.xss.enums;
/**
* XSS 模式枚举

View File

@@ -14,13 +14,14 @@
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.xss;
package top.continew.starter.security.xss.filter;
import cn.hutool.core.collection.CollUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.starter.security.xss.autoconfigure.XssProperties;
import top.continew.starter.web.util.SpringWebUtils;
import java.io.IOException;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.xss;
package top.continew.starter.security.xss.filter;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
@@ -29,7 +29,8 @@ import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.web.enums.XssMode;
import top.continew.starter.security.xss.autoconfigure.XssProperties;
import top.continew.starter.security.xss.enums.XssMode;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;

View File

@@ -0,0 +1 @@
top.continew.starter.security.xss.autoconfigure.XssAutoConfiguration

View File

@@ -17,8 +17,8 @@
<module>continew-starter-security-password</module>
<module>continew-starter-security-mask</module>
<module>continew-starter-security-crypto</module>
<module>continew-starter-security-limiter</module>
<module>continew-starter-security-sensitivewords</module>
<module>continew-starter-security-xss</module>
</modules>
<dependencies>

View File

@@ -374,7 +374,8 @@ public class OssStorageStrategy implements StorageStrategy<OssClient> {
} catch (Exception e) {
// 如果 getBucketAcl 失败,可能是权限或连接问题
log.error("获取桶 ACL 失败: {}", e.getMessage());
return true; // 出现错误时,默认认为桶是私有的
// 出现错误时,默认认为桶是私有的
return true;
}
}

View File

@@ -0,0 +1,28 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-trace</artifactId>
<description>ContiNew Starter 链路追踪模块</description>
<dependencies>
<!-- Web 模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-web</artifactId>
</dependency>
<!-- TLog轻量级的分布式日志标记追踪神器 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>tlog-web-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.trace;
package top.continew.starter.trace.autoconfigure;
/**
* TLog 配置属性

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.trace;
package top.continew.starter.trace.autoconfigure;
import com.yomahub.tlog.id.TLogIdGenerator;
import com.yomahub.tlog.id.TLogIdGeneratorLoader;
@@ -32,9 +32,11 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.trace.filter.TLogServletFilter;
import top.continew.starter.trace.handler.TraceIdGenerator;
/**
* 链路踪自动配置
* 链路踪自动配置
*
* @author Jasmine
* @author Charles7c
@@ -43,7 +45,7 @@ import top.continew.starter.core.constant.PropertiesConstants;
@AutoConfiguration
@ConditionalOnWebApplication
@EnableConfigurationProperties(TraceProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.WEB_TRACE, name = PropertiesConstants.ENABLED, havingValue = "true")
@ConditionalOnProperty(prefix = PropertiesConstants.TRACE, name = PropertiesConstants.ENABLED, havingValue = "true")
public class TraceAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(TraceAutoConfiguration.class);
@@ -89,6 +91,6 @@ public class TraceAutoConfiguration {
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Web-Trace' completed initialization.");
log.debug("[ContiNew Starter] - Auto Configuration 'Trace' completed initialization.");
}
}

View File

@@ -14,23 +14,23 @@
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.trace;
package top.continew.starter.trace.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import top.continew.starter.core.constant.PropertiesConstants;
/**
* 链路踪配置属性
* 链路踪配置属性
*
* @author Charles7c
* @since 1.3.0
*/
@ConfigurationProperties(PropertiesConstants.WEB_TRACE)
@ConfigurationProperties(PropertiesConstants.TRACE)
public class TraceProperties {
/**
* 是否启用链路踪配置
* 是否启用链路踪配置
*/
private boolean enabled = false;

View File

@@ -14,13 +14,15 @@
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.trace;
package top.continew.starter.trace.filter;
import cn.hutool.core.text.CharSequenceUtil;
import com.yomahub.tlog.context.TLogContext;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import top.continew.starter.trace.autoconfigure.TraceProperties;
import top.continew.starter.trace.handler.TLogWebCommon;
import java.io.IOException;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.trace;
package top.continew.starter.trace.handler;
import com.yomahub.tlog.constant.TLogConstants;
import com.yomahub.tlog.core.rpc.TLogLabelBean;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.trace;
package top.continew.starter.trace.handler;
import com.yomahub.tlog.id.TLogIdGenerator;
import com.yomahub.tlog.id.snowflake.UniqueIdGenerator;

View File

@@ -0,0 +1 @@
top.continew.starter.trace.autoconfigure.TraceAutoConfiguration

View File

@@ -38,12 +38,6 @@
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- TLog轻量级的分布式日志标记追踪神器 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>tlog-web-spring-boot-starter</artifactId>
</dependency>
<!-- Graceful Response一个Spring Boot技术栈下的优雅响应处理组件可以帮助开发者完成响应数据封装、异常处理、错误码填充等过程提高开发效率提高代码质量 -->
<dependency>
<groupId>com.feiniaojin</groupId>

View File

@@ -43,6 +43,7 @@ import top.continew.starter.core.constant.StringConstants;
@ConditionalOnProperty(prefix = PropertiesConstants.WEB_CORS, name = PropertiesConstants.ENABLED, havingValue = "true")
@EnableConfigurationProperties(CorsProperties.class)
public class CorsAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(CorsAutoConfiguration.class);
/**

View File

@@ -26,6 +26,11 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.continew.starter.web.autoconfigure.mvc.converter.BaseEnumConverterFactory;
import top.continew.starter.web.autoconfigure.mvc.converter.time.DateConverter;
import top.continew.starter.web.autoconfigure.mvc.converter.time.LocalDateConverter;
import top.continew.starter.web.autoconfigure.mvc.converter.time.LocalDateTimeConverter;
import top.continew.starter.web.autoconfigure.mvc.converter.time.LocalTimeConverter;
import java.util.List;
import java.util.Objects;
@@ -70,6 +75,10 @@ public class WebMvcAutoConfiguration implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new BaseEnumConverterFactory());
registry.addConverter(new DateConverter());
registry.addConverter(new LocalDateTimeConverter());
registry.addConverter(new LocalDateConverter());
registry.addConverter(new LocalTimeConverter());
}
@PostConstruct

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.mvc;
package top.continew.starter.web.autoconfigure.mvc.converter;
import org.springframework.core.convert.converter.Converter;
import top.continew.starter.core.enums.BaseEnum;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.mvc;
package top.continew.starter.web.autoconfigure.mvc.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.mvc.converter.time;
import cn.hutool.core.date.DateUtil;
import org.springframework.core.convert.converter.Converter;
import java.util.Date;
/**
* Date 参数转换器
*
* @author Charles7c
* @since 2.10.0
*/
public class DateConverter implements Converter<String, Date> {
@Override
public Date convert(String source) {
return DateUtil.parse(source);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.mvc.converter.time;
import cn.hutool.core.date.DateUtil;
import org.springframework.core.convert.converter.Converter;
import java.time.LocalDate;
/**
* LocalDate 参数转换器
*
* @author Charles7c
* @since 2.10.0
*/
public class LocalDateConverter implements Converter<String, LocalDate> {
@Override
public LocalDate convert(String source) {
return DateUtil.parse(source).toLocalDateTime().toLocalDate();
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.mvc.converter.time;
import cn.hutool.core.date.DateUtil;
import org.springframework.core.convert.converter.Converter;
import java.time.LocalDateTime;
/**
* LocalDateTime 参数转换器
*
* @author Charles7c
* @since 2.10.0
*/
public class LocalDateTimeConverter implements Converter<String, LocalDateTime> {
@Override
public LocalDateTime convert(String source) {
return DateUtil.parse(source).toLocalDateTime();
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.mvc.converter.time;
import cn.hutool.core.date.DateUtil;
import org.springframework.core.convert.converter.Converter;
import java.time.LocalTime;
/**
* LocalTime 参数转换器
*
* @author Charles7c
* @since 2.10.0
*/
public class LocalTimeConverter implements Converter<String, LocalTime> {
@Override
public LocalTime convert(String source) {
return DateUtil.parse(source).toLocalDateTime().toLocalTime();
}
}

View File

@@ -21,14 +21,13 @@ import org.apache.commons.lang3.reflect.TypeUtils;
import org.springdoc.core.parsers.ReturnTypeParser;
import org.springframework.core.MethodParameter;
import top.continew.starter.apidoc.util.DocUtils;
import top.continew.starter.web.model.R;
import java.lang.reflect.Type;
/**
* SpringDoc 全局响应处理器
* <p>
* 接口文档全局添加响应格式 {@link R}
* 接口文档全局添加响应格式 {@link com.feiniaojin.gracefulresponse.data.Response}
* </p>
*
* @author echo

View File

@@ -183,7 +183,7 @@ public class GlobalResponseAutoConfiguration {
@ConditionalOnProperty(prefix = PropertiesConstants.WEB_RESPONSE, name = "i18n", havingValue = "true")
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("i18n", "i18n/empty-messages");
messageSource.setBasenames("i18n", "i18n/messages");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setDefaultLocale(Locale.CHINA);
return messageSource;

View File

@@ -18,10 +18,13 @@ package top.continew.starter.web.model;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.feiniaojin.gracefulresponse.api.ResponseStatusFactory;
import com.feiniaojin.gracefulresponse.data.Response;
import com.feiniaojin.gracefulresponse.data.ResponseStatus;
import com.feiniaojin.gracefulresponse.defaults.DefaultResponseStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import top.continew.starter.web.autoconfigure.response.GlobalResponseProperties;
import java.util.Objects;
/**
* 响应信息
@@ -32,16 +35,15 @@ import top.continew.starter.web.autoconfigure.response.GlobalResponseProperties;
@Schema(description = "响应信息")
public class R<T> implements Response {
private static final GlobalResponseProperties PROPERTIES = SpringUtil.getBean(GlobalResponseProperties.class);
private static final String DEFAULT_SUCCESS_CODE = PROPERTIES.getDefaultSuccessCode();
private static final String DEFAULT_SUCCESS_MSG = PROPERTIES.getDefaultSuccessMsg();
private static final String DEFAULT_ERROR_CODE = PROPERTIES.getDefaultErrorCode();
private static final String DEFAULT_ERROR_MSG = PROPERTIES.getDefaultErrorMsg();
private static final ResponseStatusFactory RESPONSE_STATUS_FACTORY = SpringUtil
.getBean(ResponseStatusFactory.class);
private static final ResponseStatus DEFAULT_STATUS_SUCCESS = RESPONSE_STATUS_FACTORY.defaultSuccess();
private static final ResponseStatus DEFAULT_STATUS_ERROR = RESPONSE_STATUS_FACTORY.defaultError();
/**
* 状态码
*/
@Schema(description = "状态码", example = "1")
@Schema(description = "状态码", example = "0")
private String code;
/**
@@ -60,7 +62,7 @@ public class R<T> implements Response {
* 时间戳
*/
@Schema(description = "时间戳", example = "1691453288000")
private final Long timestamp = System.currentTimeMillis();
private Long timestamp;
/**
* 响应数据
@@ -68,29 +70,42 @@ public class R<T> implements Response {
@Schema(description = "响应数据")
private T data;
/**
* 状态信息
*/
private ResponseStatus status = new DefaultResponseStatus();
public R() {
}
public R(ResponseStatus status) {
this.status = status;
}
public R(String code, String msg) {
this.setCode(code);
this.setMsg(msg);
}
public R(ResponseStatus status, T data) {
this(status);
this.setData(data);
}
public R(String code, String msg, T data) {
this(code, msg);
this.data = data;
this.setData(data);
}
@Override
public void setStatus(ResponseStatus status) {
this.setCode(status.getCode());
this.setMsg(status.getMsg());
this.status = status;
}
@Override
@JsonIgnore
public ResponseStatus getStatus() {
return null;
return status;
}
@Override
@@ -101,24 +116,23 @@ public class R<T> implements Response {
@Override
@JsonIgnore
public Object getPayload() {
return null;
return data;
}
public String getCode() {
return code;
return status.getCode();
}
public void setCode(String code) {
this.code = code;
this.success = DEFAULT_SUCCESS_CODE.equals(code);
status.setCode(code);
}
public String getMsg() {
return msg;
return status.getMsg();
}
public void setMsg(String msg) {
this.msg = msg;
status.setMsg(msg);
}
public T getData() {
@@ -130,15 +144,11 @@ public class R<T> implements Response {
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
return Objects.equals(DEFAULT_STATUS_SUCCESS.getCode(), status.getCode());
}
public Long getTimestamp() {
return timestamp;
return System.currentTimeMillis();
}
/**
@@ -147,7 +157,7 @@ public class R<T> implements Response {
* @return R /
*/
public static R ok() {
return new R(DEFAULT_SUCCESS_CODE, DEFAULT_SUCCESS_MSG);
return new R(DEFAULT_STATUS_SUCCESS);
}
/**
@@ -157,7 +167,7 @@ public class R<T> implements Response {
* @return R /
*/
public static R ok(Object data) {
return new R(DEFAULT_SUCCESS_CODE, DEFAULT_SUCCESS_MSG, data);
return new R(DEFAULT_STATUS_SUCCESS, data);
}
/**
@@ -168,7 +178,9 @@ public class R<T> implements Response {
* @return R /
*/
public static R ok(String msg, Object data) {
return new R(DEFAULT_SUCCESS_CODE, msg, data);
R r = ok(data);
r.setMsg(msg);
return r;
}
/**
@@ -177,7 +189,7 @@ public class R<T> implements Response {
* @return R /
*/
public static R fail() {
return new R(DEFAULT_ERROR_CODE, DEFAULT_ERROR_MSG);
return new R(DEFAULT_STATUS_ERROR);
}
/**
@@ -190,4 +202,4 @@ public class R<T> implements Response {
public static R fail(String code, String msg) {
return new R(code, msg);
}
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.web.util;
import cn.hutool.core.io.IoUtil;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
* 可重复读取请求体的包装器
* 支持文件流直接透传,非文件流可重复读取
*
* @author echo
* @since 2.10.0
*/
public class RepeatReadRequestWrapper extends HttpServletRequestWrapper {
private byte[] cachedBody;
private final HttpServletRequest originalRequest;
public RepeatReadRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.originalRequest = request;
// 判断是否为文件上传请求
if (!isMultipartContent(request)) {
this.cachedBody = IoUtil.readBytes(request.getInputStream(), false);
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
// 如果是文件上传,直接返回原始输入流
if (isMultipartContent(originalRequest)) {
return originalRequest.getInputStream();
}
// 非文件上传,返回可重复读取的输入流
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
// 非阻塞I/O这里可以根据需要实现
}
@Override
public int read() {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
// 如果是文件上传直接返回原始Reader
if (isMultipartContent(originalRequest)) {
new BufferedReader(new InputStreamReader(originalRequest.getInputStream(), StandardCharsets.UTF_8));
}
return new BufferedReader(new InputStreamReader(getInputStream()));
}
/**
* 检查是否为文件上传请求
*
* @param request 请求对象
* @return 是否为文件上传请求
*/
public boolean isMultipartContent(HttpServletRequest request) {
return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart/");
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.web.util;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import org.springframework.http.MediaType;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
/**
* 可重复读取响应内容的包装器
* 支持缓存响应内容,便于日志记录和后续处理 (不缓存SSE)
*
* @author echo
* @author Charles7c
* @since 2.10.0
*/
public class RepeatReadResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream cachedOutputStream = new ByteArrayOutputStream();
private final PrintWriter writer = new PrintWriter(cachedOutputStream, true);
/**
* 是否为流式响应
*/
private boolean isStreamingResponse = false;
public RepeatReadResponseWrapper(HttpServletResponse response) {
super(response);
checkStreamingResponse();
}
@Override
public void setContentType(String type) {
super.setContentType(type);
// 根据 Content-Type 判断是否为流式响应
if (type != null) {
String lowerType = type.toLowerCase();
isStreamingResponse = lowerType.contains(MediaType.TEXT_EVENT_STREAM_VALUE);
}
}
private void checkStreamingResponse() {
String contentType = getContentType();
if (contentType != null) {
String lowerType = contentType.toLowerCase();
isStreamingResponse = lowerType.contains(MediaType.TEXT_EVENT_STREAM_VALUE);
}
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
checkStreamingResponse();
// 对于 SSE 流式响应,直接返回原始响应流,不做额外处理
if (isStreamingResponse) {
return super.getOutputStream();
}
return new ServletOutputStream() {
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
@Override
public void write(int b) throws IOException {
cachedOutputStream.write(b);
}
@Override
public void write(byte[] b) throws IOException {
cachedOutputStream.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
cachedOutputStream.write(b, off, len);
}
};
}
@Override
public PrintWriter getWriter() throws IOException {
checkStreamingResponse();
if (isStreamingResponse) {
// 对于 SSE 流式响应,直接返回原始响应写入器,不做额外处理
return super.getWriter();
}
return writer;
}
/**
* 获取缓存的响应内容
*
* @return 缓存的响应内容
*/
public String getResponseContent() {
if (!isStreamingResponse) {
writer.flush();
return cachedOutputStream.toString(StandardCharsets.UTF_8);
}
return null;
}
/**
* 将缓存的响应内容复制到原始响应中
*
* @throws IOException IO 异常
*/
public void copyBodyToResponse() throws IOException {
if (!isStreamingResponse && cachedOutputStream.size() > 0) {
getResponse().getOutputStream().write(cachedOutputStream.toByteArray());
}
}
/**
* 是否为流式响应
*
* @return 是否为流式响应
*/
public boolean isStreamingResponse() {
return isStreamingResponse;
}
}

View File

@@ -1,4 +1,2 @@
top.continew.starter.web.autoconfigure.mvc.WebMvcAutoConfiguration
top.continew.starter.web.autoconfigure.cors.CorsAutoConfiguration
top.continew.starter.web.autoconfigure.trace.TraceAutoConfiguration
top.continew.starter.web.autoconfigure.xss.XssAutoConfiguration
top.continew.starter.web.autoconfigure.cors.CorsAutoConfiguration

View File

@@ -61,8 +61,11 @@
<module>continew-starter-core</module>
<module>continew-starter-json</module>
<module>continew-starter-api-doc</module>
<module>continew-starter-security</module>
<module>continew-starter-web</module>
<module>continew-starter-security</module>
<module>continew-starter-ratelimiter</module>
<module>continew-starter-idempotent</module>
<module>continew-starter-trace</module>
<module>continew-starter-log</module>
<module>continew-starter-storage</module>
<module>continew-starter-file</module>