mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-11-12 18:57:14 +08:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ad9e08c6b | |||
|
|
1613374fcc | ||
| e294d69cdb | |||
| 89a347ffe6 | |||
| 92f5ea799e | |||
| 21262701dc | |||
| 85285e56a8 | |||
| b5bfe5c681 | |||
| 3fc9d1fbaa | |||
| 4c385927b4 | |||
|
|
da5e162a2a | ||
| 1903520433 | |||
| d9ac2764aa | |||
| c9c7c34506 | |||
| a6fb65f97e | |||
| f50b511513 | |||
| 071aef759a | |||
| 2b3de0c67e | |||
| 27a71cf076 | |||
| b199b651ec | |||
| 974efa368a | |||
|
|
199df874e5 | ||
| 591a44d861 | |||
|
|
e7d99e65aa | ||
| 0d7f777fd5 |
44
CHANGELOG.md
44
CHANGELOG.md
@@ -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)
|
||||
|
||||
### ✨ 新特性
|
||||
|
||||
@@ -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-springcache(Spring 缓存)
|
||||
├─ continew-starter-security(安全模块)
|
||||
│ ├─ continew-starter-security-crypto(加密:字段加解密)
|
||||
│ ├─ continew-starter-security-xss(XSS 过滤)
|
||||
│ ├─ 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(动态验证码)
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定缓存
|
||||
*
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- AOP(AspectJ) -->
|
||||
<dependency>
|
||||
<groupId>org.aspectj</groupId>
|
||||
<artifactId>aspectjweaver</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Hibernate Validator -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 "参数值无效";
|
||||
|
||||
/**
|
||||
* 分组
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 Cloud(Spring 团队提供的微服务解决方案) -->
|
||||
<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 S3(Amazon 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>
|
||||
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,12 +42,12 @@ public enum Api {
|
||||
/**
|
||||
* 详情
|
||||
*/
|
||||
DETAIL,
|
||||
GET,
|
||||
|
||||
/**
|
||||
* 新增
|
||||
* 创建
|
||||
*/
|
||||
ADD,
|
||||
CREATE,
|
||||
|
||||
/**
|
||||
* 修改
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 修改
|
||||
|
||||
@@ -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 分组校验-修改
|
||||
|
||||
@@ -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) {
|
||||
/* 新增后置处理 */
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
/* 新增后置处理 */
|
||||
}
|
||||
|
||||
|
||||
27
continew-starter-idempotent/pom.xml
Normal file
27
continew-starter-idempotent/pom.xml
Normal 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>
|
||||
@@ -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 "请勿重复操作";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
top.continew.starter.idempotent.autoconfigure.IdempotentAutoConfiguration
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"top.continew.starter.idempotent.annotation.Idempotent@key":{
|
||||
"method":{
|
||||
"parameters": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package top.continew.starter.log.handler;
|
||||
|
||||
import top.continew.starter.log.AbstractLogHandler;
|
||||
|
||||
/**
|
||||
* 日志处理器-AOP 版实现
|
||||
*
|
||||
|
||||
@@ -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>
|
||||
@@ -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 Noronha(Spring Boot Actuator)
|
||||
* @author Madhura Bhave(Spring 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 true:是;false:否
|
||||
*/
|
||||
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 true:是;false:否
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import java.util.Map;
|
||||
* @author Andy Wilkinson(Spring Boot Actuator)
|
||||
* @author Phillip Webb(Spring 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();
|
||||
}
|
||||
|
||||
@@ -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 Wilkinson(Spring 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) {
|
||||
|
||||
@@ -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 Wilkinson(Spring 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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否匹配放行路由
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package top.continew.starter.log.handler;
|
||||
|
||||
import top.continew.starter.log.AbstractLogHandler;
|
||||
|
||||
/**
|
||||
* 日志处理器-拦截器版实现
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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);
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.security.limiter.enums;
|
||||
package top.continew.starter.ratelimiter.enums;
|
||||
|
||||
/**
|
||||
* 限流类型
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
top.continew.starter.ratelimiter.autoconfigure.RateLimiterAutoConfiguration
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"top.continew.starter.ratelimiter.annotation.RateLimiter@key":{
|
||||
"method":{
|
||||
"parameters": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
top.continew.starter.security.limiter.autoconfigure.RateLimiterAutoConfiguration
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"top.continew.starter.security.limiter.annotation.RateLimiter@key":{
|
||||
"method":{
|
||||
"parameters": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.web.enums;
|
||||
package top.continew.starter.security.xss.enums;
|
||||
|
||||
/**
|
||||
* XSS 模式枚举
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
top.continew.starter.security.xss.autoconfigure.XssAutoConfiguration
|
||||
@@ -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>
|
||||
|
||||
@@ -374,7 +374,8 @@ public class OssStorageStrategy implements StorageStrategy<OssClient> {
|
||||
} catch (Exception e) {
|
||||
// 如果 getBucketAcl 失败,可能是权限或连接问题
|
||||
log.error("获取桶 ACL 失败: {}", e.getMessage());
|
||||
return true; // 出现错误时,默认认为桶是私有的
|
||||
// 出现错误时,默认认为桶是私有的
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
28
continew-starter-trace/pom.xml
Normal file
28
continew-starter-trace/pom.xml
Normal 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>
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.web.autoconfigure.trace;
|
||||
package top.continew.starter.trace.autoconfigure;
|
||||
|
||||
/**
|
||||
* TLog 配置属性
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
top.continew.starter.trace.autoconfigure.TraceAutoConfiguration
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
5
pom.xml
5
pom.xml
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user