mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-11-12 18:57:14 +08:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7646cd87b | |||
| 601c071505 | |||
| 47165f80a1 | |||
| 43d1489f1a | |||
|
|
5d10a28aa1 | ||
| c8e5191dc0 | |||
| 3011e64375 | |||
| 5dd6808bea | |||
| 43ba770971 | |||
|
|
586322a180 | ||
|
|
90c11f60f9 | ||
| 12746d6261 | |||
| ddd4e38dca | |||
| 35e79620e4 | |||
| d8c4224030 | |||
| a778e3182a | |||
| e4d0c98838 | |||
| c0c449870f | |||
| 67edb0828d | |||
| e05e0de7b8 | |||
| 2f2aae08ab | |||
| 3f7f118d3e | |||
| 17272a7809 | |||
| 07e1637363 | |||
| a0b64b81d5 | |||
| 778a861fa9 | |||
| d32c05166d | |||
| 4719a349dd | |||
| b4cb147a77 | |||
| aefd61b855 | |||
| 3be0d90018 | |||
| 34deff959a | |||
| 3dad27df3f | |||
| dcd185f532 |
51
CHANGELOG.md
51
CHANGELOG.md
@@ -1,3 +1,54 @@
|
||||
## [v2.13.2](https://github.com/continew-org/continew-starter/compare/v2.13.1...v2.13.2) (2025-07-21)
|
||||
|
||||
### ✨ 新特性
|
||||
|
||||
- 【core】新增 扩展 hutool TreeUtil 封装树构建的 TreeBuildUtils 工具类,其中包括扩展的(构建树形结构、构建多根节点的树结构(支持多个顶级节点))等方法。(Gitee#72@lishuyanla) ([90c11f6](https://github.com/continew-org/continew-starter/commit/90c11f60f9ba313acbfd76de66f3b4022bc8b270))
|
||||
- 【security/crypto】重构加/解密模块业务逻辑,封装 EncryptHelper 工具类,提供统一的加/解密方法,方便使用者灵活处理加/解密 (Gitee#73@lishuyanla) ([5d10a28](https://github.com/continew-org/continew-starter/commit/5d10a28aa1c4ade0a51235e302c46143b90f7bb5))
|
||||
|
||||
### 💎 功能优化
|
||||
|
||||
- 【extension/tenant】移除超级租户 ID 配置属性 ([a778e31](https://github.com/continew-org/continew-starter/commit/a778e3182a8163e9e3ea5bbc677090da2efe0a31))
|
||||
- 【extension/tenant】设置租户拦截器的优先级为最高 ([d8c4224](https://github.com/continew-org/continew-starter/commit/d8c4224030d6d2eb6eea3554e689165315924bf6))
|
||||
- 【extension/tenant】优化租户忽略逻辑 ([35e7962](https://github.com/continew-org/continew-starter/commit/35e79620e40d8d4f121a24ec720dcd8968ce9104))
|
||||
- 【extension/crud】 ([586322a](https://github.com/continew-org/continew-starter/commit/586322a180f2bce9faf1acbacb65ec09df921815))
|
||||
- 【extension/datapermission】优化数据权限模块代码 ([5dd6808](https://github.com/continew-org/continew-starter/commit/5dd6808bea4483a7e69884b69bac4928cb95bd89))
|
||||
- 【json/jackson】重构 JSON 工具类 ([43d1489](https://github.com/continew-org/continew-starter/commit/43d1489f1a850731b4fc27a2ae0cbab24a72025c))
|
||||
- 解决部分 sonar 问题 ([ddd4e38](https://github.com/continew-org/continew-starter/commit/ddd4e38dca4c5f64b9fc999d57a13d827d29d474)) ([47165f8](https://github.com/continew-org/continew-starter/commit/47165f80a15cf7da346fbbb931894284b0cd7124))
|
||||
|
||||
### 🐛 问题修复
|
||||
|
||||
- 【validation】修复字符串值仅进行了 null 判空错误 ([12746d6](https://github.com/continew-org/continew-starter/commit/12746d62613f3e9d8cce4b4aea71d6466f345e0a))
|
||||
- 【extension/tenant】将 TenantUtils.executeIgnore 方法改为静态方法 ([43ba770](https://github.com/continew-org/continew-starter/commit/43ba770971e5fb124272ed6d4fadef36be9c8fb8))
|
||||
|
||||
### 📦 依赖升级
|
||||
|
||||
- 【dependencies】spel-validator 0.5.0-beta => 0.5.1-beta ([601c071](https://github.com/continew-org/continew-starter/commit/601c0715052106f4cae3419fda0f276231cb3b13))
|
||||
|
||||
## [v2.13.1](https://github.com/continew-org/continew-starter/compare/v2.13.0...v2.13.1) (2025-07-17)
|
||||
|
||||
### ✨ 新特性
|
||||
|
||||
- 【validation】增强 EnumValue 枚举校验器,支持枚举值的数组和集合校验,增加对 BaseEnum 接口的支持 ([3dad27d](https://github.com/continew-org/continew-starter/commit/3dad27df3f747d60f47c1504286c86f6636e7242))
|
||||
- 【extension/tenant】新增 TenantIgnoreAspect 切面,完善定时任务等需要忽略租户的场景 ([07e1637](https://github.com/continew-org/continew-starter/commit/07e1637363bce4cac3f215384c8bbf6235a30778))
|
||||
- 【core】SpringUtils 工具类新增 getBean(Class<T> clazz, boolean ignoreNoSuchBeanEx) 方法 ([17272a7](https://github.com/continew-org/continew-starter/commit/17272a780905b554b1fb47e52667a51be0af7bbe))
|
||||
- 【core】新增集合工具类 CollUtils(mapToList、mapToSet) ([3f7f118](https://github.com/continew-org/continew-starter/commit/3f7f118d3e6b38c2cb13a2661a37eda3325894a7))
|
||||
- 【extension/tenant】新增 TenantUtils 替换 TenantHandler 接口及其实现类 DefaultTenantHandler ([2f2aae0](https://github.com/continew-org/continew-starter/commit/2f2aae08ab934009cd39bbe7ec3823c594fa48f8))
|
||||
- 【core】ServletUtils 新增应 JSON 数据给客户端方法 ([67edb08](https://github.com/continew-org/continew-starter/commit/67edb0828dee355ff46d055935a76a42a5a6ebd8))
|
||||
|
||||
### 💎 功能优化
|
||||
|
||||
- 【extension/crud】完善树配置相关注释 ([3be0d90](https://github.com/continew-org/continew-starter/commit/3be0d900180e4ed528f32f2350b150552aee0420))
|
||||
- 【extension/crud】移除 Crane4j 依赖方便使用者自定义实现 ([aefd61b](https://github.com/continew-org/continew-starter/commit/aefd61b855e376b0f509d34d441b1d1d5b831a39))
|
||||
- 【extension/tenant】将"多租户"描述统一为"租户" ([d32c051](https://github.com/continew-org/continew-starter/commit/d32c05166d297d1665436eae63f41277f6dca2af))
|
||||
- 【extension/tenant】将 dynamic-datasource 依赖设置为 optional ([778a861](https://github.com/continew-org/continew-starter/commit/778a861fa9f97a99a7014b72a37e984145872421))
|
||||
- 【extension/datapermission】UserContext、RoleContext 重命名为 UserData、RoleData,以避免和应用冲突 ([a0b64b8](https://github.com/continew-org/continew-starter/commit/a0b64b81d560c3c4e7175685f66ab98406a31dcc))
|
||||
- 使用 CollUtils 替代部分 Stream 集合转换 ([e05e0de](https://github.com/continew-org/continew-starter/commit/e05e0de7b81329512ea1f0ad5e9ed3c04bdfe752))
|
||||
|
||||
### 🐛 问题修复
|
||||
|
||||
- 【security/mask】修复部分注释错误 ([34deff9](https://github.com/continew-org/continew-starter/commit/34deff959aa9ba817d05b552b173b4cbaebd289a))
|
||||
- 【dependencies】指定 Apache POI 依赖版本(解决版本冲突)并移除冗余包 ([b4cb147](https://github.com/continew-org/continew-starter/commit/b4cb147a77cdfeb754c061eab888eb10314231be))
|
||||
|
||||
## [v2.13.0](https://github.com/continew-org/continew-starter/compare/v2.12.2...v2.13.0) (2025-07-05)
|
||||
|
||||
### ✨ 新特性
|
||||
|
||||
@@ -197,7 +197,7 @@ continew-starter
|
||||
├─ continew-starter-extension-datapermission(数据权限模块)
|
||||
│ ├─ continew-starter-extension-datapermission-core(核心模块)
|
||||
│ └─ continew-starter-extension-datapermission-mp(MyBatis Plus)
|
||||
├─ continew-starter-extension-tenant(多租户模块)
|
||||
├─ continew-starter-extension-tenant(租户模块)
|
||||
│ ├─ continew-starter-extension-tenant-core(核心模块)
|
||||
│ └─ continew-starter-extension-tenant-mp(MyBatis Plus)
|
||||
└─ continew-starter-extension-crud(CRUD 模块)
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import top.continew.starter.apidoc.handler.BaseEnumParameterHandler;
|
||||
import top.continew.starter.apidoc.handler.OpenApiHandler;
|
||||
import top.continew.starter.core.autoconfigure.application.ApplicationProperties;
|
||||
import top.continew.starter.core.util.CollUtils;
|
||||
import top.continew.starter.core.util.GeneralPropertySourceFactory;
|
||||
|
||||
import java.util.List;
|
||||
@@ -104,7 +105,7 @@ public class SpringDocAutoConfiguration implements WebMvcConfigurer {
|
||||
Map<String, SecurityScheme> securitySchemeMap = components.getSecuritySchemes();
|
||||
if (MapUtil.isNotEmpty(securitySchemeMap)) {
|
||||
SecurityRequirement securityRequirement = new SecurityRequirement();
|
||||
List<String> list = securitySchemeMap.values().stream().map(SecurityScheme::getName).toList();
|
||||
List<String> list = CollUtils.mapToList(securitySchemeMap.values(), SecurityScheme::getName);
|
||||
list.forEach(securityRequirement::addList);
|
||||
openApi.addSecurityItem(securityRequirement);
|
||||
}
|
||||
@@ -127,10 +128,8 @@ public class SpringDocAutoConfiguration implements WebMvcConfigurer {
|
||||
Map<String, SecurityScheme> securitySchemeMap = components.getSecuritySchemes();
|
||||
pathItem.readOperations().forEach(operation -> {
|
||||
SecurityRequirement securityRequirement = new SecurityRequirement();
|
||||
List<String> list = securitySchemeMap.values()
|
||||
.stream()
|
||||
.map(SecurityScheme::getName)
|
||||
.toList();
|
||||
List<String> list = CollUtils.mapToList(securitySchemeMap
|
||||
.values(), SecurityScheme::getName);
|
||||
list.forEach(securityRequirement::addList);
|
||||
operation.addSecurityItem(securityRequirement);
|
||||
});
|
||||
|
||||
@@ -37,11 +37,11 @@ import org.springdoc.core.utils.PropertyResolverUtils;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import top.continew.starter.core.util.CollUtils;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -176,11 +176,8 @@ public class OpenApiHandler extends OpenAPIService {
|
||||
buildTagsFromClass(handlerMethod.getBeanType(), tags, tagsStr, locale);
|
||||
|
||||
if (CollUtil.isNotEmpty(tagsStr)) {
|
||||
tagsStr = tagsStr.stream()
|
||||
.map(str -> propertyResolverUtils.resolve(str, locale))
|
||||
.collect(Collectors.toSet());
|
||||
tagsStr = CollUtils.mapToSet(tagsStr, str -> propertyResolverUtils.resolve(str, locale));
|
||||
}
|
||||
|
||||
if (springdocTags.containsKey(handlerMethod)) {
|
||||
Tag tag = springdocTags.get(handlerMethod);
|
||||
tagsStr.add(tag.getName());
|
||||
@@ -256,7 +253,7 @@ public class OpenApiHandler extends OpenAPIService {
|
||||
methodTags.addAll(AnnotatedElementUtils
|
||||
.findAllMergedAnnotations(method, io.swagger.v3.oas.annotations.tags.Tag.class));
|
||||
if (CollUtil.isNotEmpty(methodTags)) {
|
||||
tagsStr.addAll(toSet(methodTags, tag -> propertyResolverUtils.resolve(tag.name(), locale)));
|
||||
tagsStr.addAll(CollUtils.mapToSet(methodTags, tag -> propertyResolverUtils.resolve(tag.name(), locale)));
|
||||
List<io.swagger.v3.oas.annotations.tags.Tag> allTags = new ArrayList<>(methodTags);
|
||||
addTags(allTags, tags, locale);
|
||||
}
|
||||
@@ -275,22 +272,4 @@ public class OpenApiHandler extends OpenAPIService {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将collection转化为Set集合,但是两者的泛型不同<br>
|
||||
* <B>{@code Collection<E> ------> Set<T> } </B>
|
||||
*
|
||||
* @param collection 需要转化的集合
|
||||
* @param function collection中的泛型转化为set泛型的lambda表达式
|
||||
* @param <E> collection中的泛型
|
||||
* @param <T> Set中的泛型
|
||||
* @return 转化后的Set
|
||||
*/
|
||||
public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) {
|
||||
if (CollUtil.isEmpty(collection) || function == null) {
|
||||
return CollUtil.newHashSet();
|
||||
}
|
||||
return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<description>ContiNew Starter BOM</description>
|
||||
|
||||
<properties>
|
||||
<revision>2.13.0</revision>
|
||||
<revision>2.13.2</revision>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@@ -292,13 +292,13 @@
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 扩展模块 - 多租户 - MyBatis Plus ORM 模块 -->
|
||||
<!-- 扩展模块 - 租户 - MyBatis Plus ORM 模块 -->
|
||||
<dependency>
|
||||
<groupId>top.continew.starter</groupId>
|
||||
<artifactId>continew-starter-extension-tenant-mp</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<!-- 扩展模块 - 多租户 - 核心模块 -->
|
||||
<!-- 扩展模块 - 租户 - 核心模块 -->
|
||||
<dependency>
|
||||
<groupId>top.continew.starter</groupId>
|
||||
<artifactId>continew-starter-extension-tenant-core</artifactId>
|
||||
|
||||
@@ -19,9 +19,10 @@ package top.continew.starter.core.constant;
|
||||
/**
|
||||
* 字符相关常量
|
||||
*
|
||||
* @see cn.hutool.core.text.CharPool
|
||||
*
|
||||
* @author looly(<a href="https://gitee.com/dromara/hutool">Hutool</a>)
|
||||
* @author Charles7c
|
||||
* @see cn.hutool.core.text.CharPool
|
||||
* @since 2.7.3
|
||||
*/
|
||||
public class CharConstants {
|
||||
|
||||
@@ -150,7 +150,7 @@ public class PropertiesConstants {
|
||||
public static final String DATA_PERMISSION = CONTINEW_STARTER + StringConstants.DOT + "data-permission";
|
||||
|
||||
/**
|
||||
* 多租户配置
|
||||
* 租户配置
|
||||
*/
|
||||
public static final String TENANT = CONTINEW_STARTER + StringConstants.DOT + "tenant";
|
||||
|
||||
|
||||
@@ -19,9 +19,10 @@ package top.continew.starter.core.constant;
|
||||
/**
|
||||
* 字符串相关常量
|
||||
*
|
||||
* @see cn.hutool.core.text.StrPool
|
||||
*
|
||||
* @author looly(<a href="https://gitee.com/dromara/hutool">Hutool</a>)
|
||||
* @author Charles7c
|
||||
* @see cn.hutool.core.text.StrPool
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class StringConstants {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 集合相关工具类
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.13.1
|
||||
*/
|
||||
public class CollUtils {
|
||||
|
||||
private CollUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 func 自定义一个规则,此规则将原集合中的元素转换成新的元素,生成新的列表返回<br>
|
||||
* 例如:提供一个 Bean 列表,通过 Function 接口实现获取某个字段值,返回这个字段值组成的新列表
|
||||
*
|
||||
* @param <T> 集合元素类型
|
||||
* @param <R> 返回集合元素类型
|
||||
* @param collection 原集合
|
||||
* @param func 编辑函数
|
||||
* @return 抽取后的新列表(默认去除 null 值)
|
||||
* @see CollUtil#map(Iterable, Function, boolean)
|
||||
*/
|
||||
public static <T, R> List<R> mapToList(Collection<T> collection, Function<? super T, ? extends R> func) {
|
||||
return mapToList(collection, func, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 func 自定义一个规则,此规则将原集合中的元素转换成新的元素,生成新的列表返回<br>
|
||||
* 例如:提供一个 Bean 列表,通过 Function 接口实现获取某个字段值,返回这个字段值组成的新列表
|
||||
*
|
||||
* @param <T> 集合元素类型
|
||||
* @param <R> 返回集合元素类型
|
||||
* @param collection 原集合
|
||||
* @param func 编辑函数
|
||||
* @param ignoreNull 是否忽略空值,这里的空值包括函数处理前和处理后的 null 值
|
||||
* @return 抽取后的新列表
|
||||
* @see CollUtil#map(Iterable, Function, boolean)
|
||||
*/
|
||||
public static <T, R> List<R> mapToList(Collection<T> collection,
|
||||
Function<? super T, ? extends R> func,
|
||||
boolean ignoreNull) {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
Stream<T> stream = collection.stream();
|
||||
if (ignoreNull) {
|
||||
return stream.filter(Objects::nonNull).map(func).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
}
|
||||
return stream.map(func).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 func 自定义一个规则,此规则将原集合中的元素转换成新的元素,生成新的集合返回<br>
|
||||
* 例如:提供一个 Bean 集合,通过 Function 接口实现获取某个字段值,返回这个字段值组成的新集合
|
||||
*
|
||||
* @param <T> 集合元素类型
|
||||
* @param <R> 返回集合元素类型
|
||||
* @param collection 原集合
|
||||
* @param func 编辑函数
|
||||
* @return 抽取后的新集合(默认去除 null 值)
|
||||
* @see CollUtil#map(Iterable, Function, boolean)
|
||||
*/
|
||||
public static <T, R> Set<R> mapToSet(Collection<T> collection, Function<? super T, ? extends R> func) {
|
||||
return mapToSet(collection, func, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 func 自定义一个规则,此规则将原集合中的元素转换成新的元素,生成新的集合返回<br>
|
||||
* 例如:提供一个 Bean 集合,通过 Function 接口实现获取某个字段值,返回这个字段值组成的新集合
|
||||
*
|
||||
* @param <T> 集合元素类型
|
||||
* @param <R> 返回集合元素类型
|
||||
* @param collection 原集合
|
||||
* @param func 编辑函数
|
||||
* @param ignoreNull 是否忽略空值,这里的空值包括函数处理前和处理后的 null 值
|
||||
* @return 抽取后的新集合
|
||||
* @see CollUtil#map(Iterable, Function, boolean)
|
||||
*/
|
||||
public static <T, R> Set<R> mapToSet(Collection<T> collection,
|
||||
Function<? super T, ? extends R> func,
|
||||
boolean ignoreNull) {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return new HashSet<>(0);
|
||||
}
|
||||
Stream<T> stream = collection.stream();
|
||||
if (ignoreNull) {
|
||||
return stream.filter(Objects::nonNull).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
}
|
||||
return stream.map(func).collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ public class ReflectUtils {
|
||||
*/
|
||||
public static List<String> getNonStaticFieldsName(Class<?> beanClass) throws SecurityException {
|
||||
List<Field> nonStaticFields = getNonStaticFields(beanClass);
|
||||
return nonStaticFields.stream().map(Field::getName).collect(Collectors.toList());
|
||||
return CollUtils.mapToList(nonStaticFields, Field::getName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,7 @@ import cn.hutool.json.JSONUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
@@ -36,7 +37,10 @@ import top.continew.starter.core.wrapper.RepeatReadResponseWrapper;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Servlet 工具类
|
||||
@@ -334,6 +338,18 @@ public class ServletUtils extends JakartaServletUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应 JSON 数据给客户端
|
||||
*
|
||||
* @param response 响应对象
|
||||
* @param data 响应数据
|
||||
* @since 2.13.1
|
||||
* @see #write(HttpServletResponse, String, String)
|
||||
*/
|
||||
public static void writeJSON(HttpServletResponse response, String data) {
|
||||
write(response, data, MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加查询字符串
|
||||
*
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package top.continew.starter.core.util;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
|
||||
/**
|
||||
* Spring 工具类
|
||||
@@ -40,4 +41,25 @@ public class SpringUtils {
|
||||
public static <T> T getProxy(T target) {
|
||||
return (T)SpringUtil.getBean(target.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 class 获取 Bean
|
||||
*
|
||||
* @param <T> Bean类型
|
||||
* @param clazz Bean类
|
||||
* @param ignoreNoSuchBeanEx 是否忽略 {@link NoSuchBeanDefinitionException}
|
||||
* @return Bean对象
|
||||
* @see SpringUtil#getBean(Class)
|
||||
* @since 2.13.1
|
||||
*/
|
||||
public static <T> T getBean(Class<T> clazz, boolean ignoreNoSuchBeanEx) {
|
||||
try {
|
||||
return SpringUtil.getBean(clazz);
|
||||
} catch (NoSuchBeanDefinitionException e) {
|
||||
if (ignoreNoSuchBeanEx) {
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.tree.Tree;
|
||||
import cn.hutool.core.lang.tree.TreeNodeConfig;
|
||||
import cn.hutool.core.lang.tree.TreeUtil;
|
||||
import cn.hutool.core.lang.tree.parser.NodeParser;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 扩展 hutool TreeUtil 封装树构建
|
||||
*
|
||||
* @author Lion Li
|
||||
* @author lishuyan
|
||||
*/
|
||||
public class TreeBuildUtils extends TreeUtil {
|
||||
|
||||
private TreeBuildUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建树形结构
|
||||
*
|
||||
* @param <T> 输入节点的类型
|
||||
* @param <K> 节点ID的类型
|
||||
* @param list 节点列表,其中包含了要构建树形结构的所有节点
|
||||
* @param nodeParser 解析器,用于将输入节点转换为树节点
|
||||
* @return 构建好的树形结构列表
|
||||
*/
|
||||
public static <T, K> List<Tree<K>> build(List<T> list, NodeParser<T, K> nodeParser) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
K k = ReflectUtil.invoke(list.get(0), CharSequenceUtil.genGetter("parentId"));
|
||||
return TreeUtil.build(list, k, TreeNodeConfig.DEFAULT_CONFIG, nodeParser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建树形结构
|
||||
*
|
||||
* @param <T> 输入节点的类型
|
||||
* @param <K> 节点ID的类型
|
||||
* @param parentId 顶级节点
|
||||
* @param list 节点列表,其中包含了要构建树形结构的所有节点
|
||||
* @param nodeParser 解析器,用于将输入节点转换为树节点
|
||||
* @return 构建好的树形结构列表
|
||||
*/
|
||||
public static <T, K> List<Tree<K>> build(List<T> list, K parentId, NodeParser<T, K> nodeParser) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return TreeUtil.build(list, parentId, TreeNodeConfig.DEFAULT_CONFIG, nodeParser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建树形结构
|
||||
*
|
||||
* @param <T> 输入节点的类型
|
||||
* @param <K> 节点ID的类型
|
||||
* @param list 节点列表,其中包含了要构建树形结构的所有节点
|
||||
* @param parentId 顶级节点
|
||||
* @param treeNodeConfig 树节点配置
|
||||
* @param nodeParser 解析器,用于将输入节点转换为树节点
|
||||
* @return 构建好的树形结构列表
|
||||
*/
|
||||
public static <T, K> List<Tree<K>> build(List<T> list,
|
||||
K parentId,
|
||||
TreeNodeConfig treeNodeConfig,
|
||||
NodeParser<T, K> nodeParser) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return TreeUtil.build(list, parentId, treeNodeConfig, nodeParser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建多根节点的树结构(支持多个顶级节点)
|
||||
*
|
||||
* @param list 原始数据列表
|
||||
* @param getId 获取节点 ID 的方法引用,例如:node -> node.getId()
|
||||
* @param getParentId 获取节点父级 ID 的方法引用,例如:node -> node.getParentId()
|
||||
* @param parser 树节点属性映射器,用于将原始节点 T 转为 Tree 节点
|
||||
* @param <T> 原始数据类型(如实体类、DTO 等)
|
||||
* @param <K> 节点 ID 类型(如 Long、String)
|
||||
* @return 构建完成的树形结构(可能包含多个顶级根节点)
|
||||
*/
|
||||
public static <T, K> List<Tree<K>> buildMultiRoot(List<T> list,
|
||||
Function<T, K> getId,
|
||||
Function<T, K> getParentId,
|
||||
NodeParser<T, K> parser) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
Set<K> rootParentIds = CollUtils.mapToSet(list, getParentId);
|
||||
rootParentIds.removeAll(CollUtils.mapToSet(list, getId));
|
||||
// 构建每一个根 parentId 下的树,并合并成最终结果列表
|
||||
return rootParentIds.stream()
|
||||
.flatMap(rootParentId -> TreeUtil.build(list, rootParentId, parser).stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建多根节点的树结构(支持多个顶级节点)
|
||||
*
|
||||
* @param <T> 原始数据类型(如实体类、DTO 等)
|
||||
* @param <K> 节点 ID 类型(如 Long、String)
|
||||
* @param list 原始数据列表
|
||||
* @param getId 获取节点 ID 的方法引用,例如:node -> node.getId()
|
||||
* @param getParentId 获取节点父级 ID 的方法引用,例如:node -> node.getParentId()
|
||||
* @param treeNodeConfig 树节点配置
|
||||
* @param parser 树节点属性映射器,用于将原始节点 T 转为 Tree 节点
|
||||
* @return 构建完成的树形结构(可能包含多个顶级根节点)
|
||||
*/
|
||||
public static <T, K> List<Tree<K>> buildMultiRoot(List<T> list,
|
||||
Function<T, K> getId,
|
||||
Function<T, K> getParentId,
|
||||
TreeNodeConfig treeNodeConfig,
|
||||
NodeParser<T, K> parser) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
Set<K> rootParentIds = CollUtils.mapToSet(list, getParentId);
|
||||
rootParentIds.removeAll(CollUtils.mapToSet(list, getId));
|
||||
// 构建每一个根 parentId 下的树,并合并成最终结果列表
|
||||
return rootParentIds.stream()
|
||||
.flatMap(rootParentId -> TreeUtil.build(list, rootParentId, treeNodeConfig, parser).stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点列表中所有节点的叶子节点
|
||||
*
|
||||
* @param <K> 节点ID的类型
|
||||
* @param nodes 节点列表
|
||||
* @return 包含所有叶子节点的列表
|
||||
*/
|
||||
public static <K> List<Tree<K>> getLeafNodes(List<Tree<K>> nodes) {
|
||||
if (CollUtil.isEmpty(nodes)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return nodes.stream().flatMap(TreeBuildUtils::extractLeafNodes).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定节点下的所有叶子节点
|
||||
*
|
||||
* @param <K> 节点ID的类型
|
||||
* @param node 要查找叶子节点的根节点
|
||||
* @return 包含所有叶子节点的列表
|
||||
*/
|
||||
private static <K> Stream<Tree<K>> extractLeafNodes(Tree<K> node) {
|
||||
if (!node.hasChild()) {
|
||||
return Stream.of(node);
|
||||
} else {
|
||||
// 递归调用,获取所有子节点的叶子节点
|
||||
return node.getChildren().stream().flatMap(TreeBuildUtils::extractLeafNodes);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,8 +25,9 @@ import java.util.function.BooleanSupplier;
|
||||
/**
|
||||
* 业务参数校验工具类(抛出 500 ServiceException)
|
||||
*
|
||||
* @author Charles7c
|
||||
* @see BusinessException
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class CheckUtils extends Validator {
|
||||
|
||||
@@ -24,8 +24,9 @@ import java.util.function.BooleanSupplier;
|
||||
/**
|
||||
* 基本参数校验工具类(抛出 400 BadRequestException)
|
||||
*
|
||||
* @see top.continew.starter.core.exception.BadRequestException
|
||||
*
|
||||
* @author Charles7c
|
||||
* @see BadRequestException
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class ValidationUtils extends Validator {
|
||||
|
||||
@@ -33,9 +33,10 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
/**
|
||||
* 复合枚举类型处理器(扩展 BaseEnum 支持)
|
||||
*
|
||||
* @see com.baomidou.mybatisplus.core.handlers.CompositeEnumTypeHandler
|
||||
*
|
||||
* @author miemie(<a href="https://gitee.com/baomidou/mybatis-plus">MyBatis Plus</a>)
|
||||
* @author Charles7c
|
||||
* @see com.baomidou.mybatisplus.core.handlers.CompositeEnumTypeHandler
|
||||
* @since 2.7.3
|
||||
*/
|
||||
public class CompositeBaseEnumTypeHandler<E extends Enum<E>> implements TypeHandler<E> {
|
||||
|
||||
@@ -45,9 +45,10 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
/**
|
||||
* 枚举类型处理器(扩展 BaseEnum 支持)
|
||||
*
|
||||
* @see com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
|
||||
*
|
||||
* @author hubin(<a href="https://gitee.com/baomidou/mybatis-plus">MyBatis Plus</a>)
|
||||
* @author Charles7c
|
||||
* @see com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
|
||||
* @since 2.4.0
|
||||
*/
|
||||
public class MybatisBaseEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
|
||||
|
||||
@@ -55,6 +55,7 @@ import java.util.function.Function;
|
||||
* <p>将 MP 的 {@link CrudRepository} 迁移至本类中,减少两层继承,解决层级过多出现 Sonar 警告的问题</p>
|
||||
*
|
||||
* @see CrudRepository
|
||||
*
|
||||
* @param <M> Mapper 接口
|
||||
* @param <T> 实体类型
|
||||
* @author hubin (<a href="https://gitee.com/baomidou/mybatis-plus">MyBatis Plus</a>)
|
||||
|
||||
@@ -13,47 +13,77 @@
|
||||
<description>ContiNew Starter 依赖模块</description>
|
||||
|
||||
<properties>
|
||||
<!-- 项目版本号 -->
|
||||
<revision>2.13.0</revision>
|
||||
<!-- Project Version -->
|
||||
<revision>2.13.2</revision>
|
||||
|
||||
<!-- Core Framework Versions -->
|
||||
<spring-boot.version>3.3.12</spring-boot.version>
|
||||
<spring-cloud.version>2023.0.5</spring-cloud.version>
|
||||
|
||||
<!-- Cache and Storage Versions -->
|
||||
<redisson.version>3.49.0</redisson.version>
|
||||
<jetcache.version>2.7.8</jetcache.version>
|
||||
<cosid.version>2.13.0</cosid.version>
|
||||
|
||||
<!-- Security and Authentication Versions -->
|
||||
<sa-token.version>1.44.0</sa-token.version>
|
||||
<just-auth.version>1.16.7</just-auth.version>
|
||||
|
||||
<!-- Database and ORM Versions -->
|
||||
<mybatis-plus.version>3.5.12</mybatis-plus.version>
|
||||
<mybatis-flex.version>1.10.9</mybatis-flex.version>
|
||||
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
|
||||
<p6spy.version>3.9.1</p6spy.version>
|
||||
|
||||
<!-- ID Generator and Job Scheduler Versions -->
|
||||
<cosid.version>2.13.0</cosid.version>
|
||||
<snail-job.version>1.5.0</snail-job.version>
|
||||
|
||||
<!-- Messaging and Notification Versions -->
|
||||
<sms4j.version>3.3.5</sms4j.version>
|
||||
|
||||
<!-- Captcha Versions -->
|
||||
<aj-captcha.version>1.4.0</aj-captcha.version>
|
||||
<easy-captcha.version>1.6.2</easy-captcha.version>
|
||||
<nashorn.version>15.6</nashorn.version>
|
||||
|
||||
<!-- Excel Processing Versions -->
|
||||
<fastexcel.version>1.2.0</fastexcel.version>
|
||||
<poi.version>5.4.1</poi.version>
|
||||
|
||||
<!-- File Storage Versions -->
|
||||
<x-file-storage.version>2.2.1</x-file-storage.version>
|
||||
<aws-s3-v1.version>1.12.783</aws-s3-v1.version>
|
||||
<aws-sdk.version>2.31.63</aws-sdk.version>
|
||||
<aws-crt.version>0.38.5</aws-crt.version>
|
||||
<thumbnails.version>0.4.20</thumbnails.version>
|
||||
|
||||
<!-- Validation and Response Processing Versions -->
|
||||
<graceful-response.version>5.0.5-boot3</graceful-response.version>
|
||||
<spel-validator.version>0.5.0-beta</spel-validator.version>
|
||||
<spel-validator.version>0.5.1-beta</spel-validator.version>
|
||||
<crane4j.version>2.9.0</crane4j.version>
|
||||
|
||||
<!-- API Documentation Versions -->
|
||||
<knife4j.version>4.5.0</knife4j.version>
|
||||
|
||||
<!-- Tracing and Logging Versions -->
|
||||
<tlog.version>1.5.2</tlog.version>
|
||||
|
||||
<!-- License and Compression Versions -->
|
||||
<truelicense.version>1.33</truelicense.version>
|
||||
<zip4j.version>2.11.5</zip4j.version>
|
||||
|
||||
<!-- HTTP Client and Utilities Versions -->
|
||||
<okhttp.version>4.12.0</okhttp.version>
|
||||
<ttl.version>2.14.5</ttl.version>
|
||||
<ip2region.version>3.3.6</ip2region.version>
|
||||
<hutool.version>5.8.38</hutool.version>
|
||||
<snakeyaml.version>2.4</snakeyaml.version>
|
||||
<!-- 解决部分传递依赖漏洞问题 -->
|
||||
<nashorn.version>15.6</nashorn.version>
|
||||
|
||||
<!-- Security Vulnerability Fix Versions -->
|
||||
<commons-beanutils.version>1.11.0</commons-beanutils.version>
|
||||
<commons-io.version>2.17.0</commons-io.version>
|
||||
<commons-compress.version>1.26.0</commons-compress.version>
|
||||
|
||||
<!-- Maven Plugin Versions -->
|
||||
<flatten.version>1.7.0</flatten.version>
|
||||
<spotless.version>2.44.3</spotless.version>
|
||||
@@ -217,10 +247,31 @@
|
||||
</dependency>
|
||||
|
||||
<!-- Apache POI(适用于 Microsoft 文档的 Java API) -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi</artifactId>
|
||||
<version>${poi.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>${poi.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- X File Storage(一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS...等其它兼容 S3 协议的存储平台) -->
|
||||
|
||||
@@ -28,8 +28,9 @@ import top.continew.starter.core.enums.BaseEnum;
|
||||
/**
|
||||
* Easy Excel 枚举接口转换器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @see BaseEnum
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public class ExcelBaseEnumConverter implements Converter<BaseEnum<?>> {
|
||||
|
||||
@@ -39,11 +39,5 @@
|
||||
<groupId>top.continew.starter</groupId>
|
||||
<artifactId>continew-starter-excel-fastexcel</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Crane4j(基于注解的,用于完成一切 “根据 A 的 key 值拿到 B,再把 B 的属性映射到 A” 这类需求的字段填充框架) -->
|
||||
<dependency>
|
||||
<groupId>cn.crane4j</groupId>
|
||||
<artifactId>crane4j-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -16,13 +16,21 @@
|
||||
|
||||
package top.continew.starter.extension.crud.annotation;
|
||||
|
||||
import top.continew.starter.extension.crud.autoconfigure.CrudTreeProperties;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 树结构字段
|
||||
*
|
||||
* @author Charles7c
|
||||
* <p>
|
||||
* 用于复杂树场景,例如:表格
|
||||
* </p>
|
||||
*
|
||||
* @see cn.hutool.core.lang.tree.TreeNodeConfig
|
||||
* @see CrudTreeProperties
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
|
||||
@@ -31,6 +31,7 @@ public class CrudProperties {
|
||||
|
||||
/**
|
||||
* 树配置
|
||||
* <p>用于简单树场景,例如:树选择(下拉)</p>
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private CrudTreeProperties tree = new CrudTreeProperties();
|
||||
|
||||
@@ -23,6 +23,12 @@ import top.continew.starter.extension.crud.annotation.TreeField;
|
||||
/**
|
||||
* CRUD 树列表配置属性
|
||||
*
|
||||
* <p>
|
||||
* 用于简单树场景,例如:树选择(下拉)
|
||||
* </p>
|
||||
*
|
||||
* @see TreeField
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.7.2
|
||||
*/
|
||||
|
||||
@@ -61,10 +61,6 @@ public interface CrudService<L, D, Q, C> {
|
||||
|
||||
/**
|
||||
* 查询树列表
|
||||
* <p>
|
||||
* 虽然提供了查询条件,但不建议使用,容易因缺失根节点导致树节点丢失。
|
||||
* 建议在前端进行查询过滤,如需使用建议重写方法。
|
||||
* </p>
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @param sortQuery 排序查询条件
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package top.continew.starter.extension.crud.service;
|
||||
|
||||
import cn.crane4j.core.support.OperateTemplate;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.bean.copier.CopyOptions;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
@@ -43,6 +42,7 @@ import top.continew.starter.extension.crud.autoconfigure.CrudTreeProperties;
|
||||
import top.continew.starter.extension.crud.model.entity.BaseIdDO;
|
||||
import top.continew.starter.extension.crud.model.query.PageQuery;
|
||||
import top.continew.starter.extension.crud.model.query.SortQuery;
|
||||
import top.continew.starter.extension.crud.model.resp.LabelValueResp;
|
||||
import top.continew.starter.extension.crud.model.resp.PageResp;
|
||||
import top.continew.starter.excel.util.ExcelUtils;
|
||||
|
||||
@@ -63,7 +63,7 @@ import java.util.Optional;
|
||||
* @author Charles7c
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D, Q, C> extends ServiceImpl<M, T> implements CrudService<L, D, Q, C> {
|
||||
public class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D, Q, C> extends ServiceImpl<M, T> implements CrudService<L, D, Q, C> {
|
||||
|
||||
protected final Class<L> listClass = this.currentListClass();
|
||||
protected final Class<D> detailClass = this.currentDetailClass();
|
||||
@@ -131,6 +131,11 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
|
||||
return detail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LabelValueResp> listDict(Q query, SortQuery sortQuery) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(C req) {
|
||||
@@ -214,19 +219,6 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充数据
|
||||
*
|
||||
* @param obj 待填充信息
|
||||
*/
|
||||
protected void fill(Object obj) {
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
OperateTemplate operateTemplate = SpringUtil.getBean(OperateTemplate.class);
|
||||
operateTemplate.execute(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 QueryWrapper
|
||||
*
|
||||
@@ -239,6 +231,14 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
|
||||
return QueryWrapperHelper.build(query, queryFields, queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充数据
|
||||
*
|
||||
* @param obj 待填充信息
|
||||
*/
|
||||
protected void fill(Object obj) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增前置处理
|
||||
*
|
||||
|
||||
@@ -16,14 +16,12 @@
|
||||
|
||||
package top.continew.starter.extension.crud.service;
|
||||
|
||||
import cn.crane4j.core.support.OperateTemplate;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.bean.copier.CopyOptions;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.lang.tree.Tree;
|
||||
import cn.hutool.core.lang.tree.TreeNodeConfig;
|
||||
import cn.hutool.core.lang.tree.TreeUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
@@ -37,9 +35,13 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.util.ClassUtils;
|
||||
import top.continew.starter.core.util.ReflectUtils;
|
||||
import top.continew.starter.core.util.TreeBuildUtils;
|
||||
import top.continew.starter.core.util.validation.CheckUtils;
|
||||
import top.continew.starter.core.util.validation.ValidationUtils;
|
||||
import top.continew.starter.data.mapper.BaseMapper;
|
||||
import top.continew.starter.data.service.impl.ServiceImpl;
|
||||
import top.continew.starter.data.util.QueryWrapperHelper;
|
||||
import top.continew.starter.excel.util.ExcelUtils;
|
||||
import top.continew.starter.extension.crud.annotation.DictModel;
|
||||
import top.continew.starter.extension.crud.annotation.TreeField;
|
||||
import top.continew.starter.extension.crud.autoconfigure.CrudProperties;
|
||||
@@ -49,12 +51,13 @@ import top.continew.starter.extension.crud.model.query.PageQuery;
|
||||
import top.continew.starter.extension.crud.model.query.SortQuery;
|
||||
import top.continew.starter.extension.crud.model.resp.LabelValueResp;
|
||||
import top.continew.starter.extension.crud.model.resp.PageResp;
|
||||
import top.continew.starter.excel.util.ExcelUtils;
|
||||
import top.continew.starter.core.util.validation.CheckUtils;
|
||||
import top.continew.starter.core.util.validation.ValidationUtils;
|
||||
|
||||
import java.lang.invoke.MethodHandleProxies;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* CRUD 业务实现基类
|
||||
@@ -68,7 +71,7 @@ import java.util.*;
|
||||
* @author Charles7c
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D, Q, C> extends ServiceImpl<M, T> implements CrudService<L, D, Q, C> {
|
||||
public class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D, Q, C> extends ServiceImpl<M, T> implements CrudService<L, D, Q, C> {
|
||||
|
||||
private Class<L> listClass;
|
||||
private Class<D> detailClass;
|
||||
@@ -101,20 +104,18 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
|
||||
CrudProperties crudProperties = SpringUtil.getBean(CrudProperties.class);
|
||||
CrudTreeProperties treeProperties = crudProperties.getTree();
|
||||
TreeField treeField = listClass.getDeclaredAnnotation(TreeField.class);
|
||||
TreeNodeConfig treeNodeConfig;
|
||||
Long rootId;
|
||||
// 简单树(下拉列表)使用全局配置结构,复杂树(表格)使用局部配置
|
||||
if (isSimple) {
|
||||
treeNodeConfig = treeProperties.genTreeNodeConfig();
|
||||
rootId = treeProperties.getRootId();
|
||||
} else {
|
||||
treeNodeConfig = treeProperties.genTreeNodeConfig(treeField);
|
||||
rootId = treeField.rootId();
|
||||
}
|
||||
TreeNodeConfig treeNodeConfig = isSimple
|
||||
? treeProperties.genTreeNodeConfig()
|
||||
: treeProperties.genTreeNodeConfig(treeField);
|
||||
String valueGetter = CharSequenceUtil.genGetter(treeField.value());
|
||||
String parentIdKeyGetter = CharSequenceUtil.genGetter(treeField.parentIdKey());
|
||||
Function<L, Long> getId = createMethodReference(listClass, valueGetter);
|
||||
Function<L, Long> getParentId = createMethodReference(listClass, parentIdKeyGetter);
|
||||
// 构建树
|
||||
return TreeUtil.build(list, rootId, treeNodeConfig, (node, tree) -> {
|
||||
tree.setId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.value())));
|
||||
tree.setParentId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.parentIdKey())));
|
||||
return TreeBuildUtils.buildMultiRoot(list, getId, getParentId, treeNodeConfig, (node, tree) -> {
|
||||
tree.setId(ReflectUtil.invoke(node, valueGetter));
|
||||
tree.setParentId(ReflectUtil.invoke(node, parentIdKeyGetter));
|
||||
tree.setName(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.nameKey())));
|
||||
tree.setWeight(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.weightKey())));
|
||||
// 如果构建简单树结构,则不包含扩展字段
|
||||
@@ -128,6 +129,26 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过反射创建方法引用
|
||||
*
|
||||
* @param clazz 实体类类型
|
||||
* @param methodName 方法名
|
||||
* @param <T> 实体类类型
|
||||
* @param <K> 返回值类型
|
||||
* @return Function<T, K> 方法引用
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T, K> Function<T, K> createMethodReference(Class<T> clazz, String methodName) {
|
||||
try {
|
||||
Method method = clazz.getDeclaredMethod(methodName);
|
||||
method.setAccessible(true);
|
||||
return MethodHandleProxies.asInterfaceInstance(Function.class, MethodHandles.lookup().unreflect(method));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to create method reference for " + methodName, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public D get(Long id) {
|
||||
T entity = super.getById(id, false);
|
||||
@@ -304,19 +325,6 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充数据
|
||||
*
|
||||
* @param obj 待填充信息
|
||||
*/
|
||||
protected void fill(Object obj) {
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
OperateTemplate operateTemplate = SpringUtil.getBean(OperateTemplate.class);
|
||||
operateTemplate.execute(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 QueryWrapper
|
||||
*
|
||||
@@ -329,6 +337,14 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
|
||||
return QueryWrapperHelper.build(query, this.getQueryFields(), queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充数据
|
||||
*
|
||||
* @param obj 待填充信息
|
||||
*/
|
||||
protected void fill(Object obj) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增前置处理
|
||||
*
|
||||
|
||||
@@ -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.extension.datapermission.constant;
|
||||
|
||||
/**
|
||||
* 数据权限常量
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.13.2
|
||||
*/
|
||||
public final class DataPermissionConstants {
|
||||
|
||||
/**
|
||||
* 数据库字段:祖先节点
|
||||
*/
|
||||
public static final String ANCESTORS_COLUMN = "ancestors";
|
||||
|
||||
/**
|
||||
* 方法名后缀:COUNT
|
||||
*/
|
||||
public static final String COUNT_METHOD_SUFFIX = "_COUNT";
|
||||
|
||||
private DataPermissionConstants() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.extension.datapermission.exception;
|
||||
|
||||
import top.continew.starter.core.exception.BaseException;
|
||||
|
||||
/**
|
||||
* 数据权限异常
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.13.2
|
||||
*/
|
||||
public class DataPermissionException extends BaseException {
|
||||
|
||||
public DataPermissionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DataPermissionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public static DataPermissionException unsupportedDataScope(String dataScope) {
|
||||
return new DataPermissionException("Unsupported data scope: " + dataScope);
|
||||
}
|
||||
|
||||
public static DataPermissionException unsupportedDatabase(String database) {
|
||||
return new DataPermissionException("Unsupported database for data permission: " + database);
|
||||
}
|
||||
|
||||
public static DataPermissionException invalidUserData(String message) {
|
||||
return new DataPermissionException("Invalid user data: " + message);
|
||||
}
|
||||
|
||||
public static DataPermissionException methodNotFound(String mappedStatementId) {
|
||||
return new DataPermissionException("Method not found for data permission: " + mappedStatementId);
|
||||
}
|
||||
}
|
||||
@@ -19,36 +19,36 @@ package top.continew.starter.extension.datapermission.model;
|
||||
import top.continew.starter.extension.datapermission.enums.DataScope;
|
||||
|
||||
/**
|
||||
* 角色上下文
|
||||
* 角色数据
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public class RoleContext {
|
||||
public class RoleData {
|
||||
|
||||
/**
|
||||
* 角色 ID
|
||||
*/
|
||||
private String roleId;
|
||||
private Long roleId;
|
||||
|
||||
/**
|
||||
* 数据权限
|
||||
*/
|
||||
private DataScope dataScope;
|
||||
|
||||
public RoleContext() {
|
||||
public RoleData() {
|
||||
}
|
||||
|
||||
public RoleContext(String roleId, DataScope dataScope) {
|
||||
public RoleData(Long roleId, DataScope dataScope) {
|
||||
this.roleId = roleId;
|
||||
this.dataScope = dataScope;
|
||||
}
|
||||
|
||||
public String getRoleId() {
|
||||
public Long getRoleId() {
|
||||
return roleId;
|
||||
}
|
||||
|
||||
public void setRoleId(String roleId) {
|
||||
public void setRoleId(Long roleId) {
|
||||
this.roleId = roleId;
|
||||
}
|
||||
|
||||
@@ -59,4 +59,20 @@ public class RoleContext {
|
||||
public void setDataScope(DataScope dataScope) {
|
||||
this.dataScope = dataScope;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
RoleData roleData = (RoleData)o;
|
||||
return roleId.equals(roleData.roleId) && dataScope == roleData.dataScope;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = roleId.hashCode();
|
||||
result = 31 * result + dataScope.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -16,52 +16,71 @@
|
||||
|
||||
package top.continew.starter.extension.datapermission.model;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 用户上下文
|
||||
* 用户数据
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public class UserContext {
|
||||
public class UserData {
|
||||
|
||||
/**
|
||||
* 用户 ID
|
||||
*/
|
||||
private String userId;
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 角色列表
|
||||
*/
|
||||
private Set<RoleContext> roles;
|
||||
private Set<RoleData> roles = Collections.emptySet();
|
||||
|
||||
/**
|
||||
* 部门 ID
|
||||
*/
|
||||
private String deptId;
|
||||
private Long deptId;
|
||||
|
||||
public String getUserId() {
|
||||
public UserData() {
|
||||
}
|
||||
|
||||
public UserData(Long userId, Long deptId, Set<RoleData> roles) {
|
||||
this.userId = userId;
|
||||
this.deptId = deptId;
|
||||
this.roles = roles != null ? roles : Collections.emptySet();
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public Set<RoleContext> getRoles() {
|
||||
public Set<RoleData> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public void setRoles(Set<RoleContext> roles) {
|
||||
this.roles = roles;
|
||||
public void setRoles(Set<RoleData> roles) {
|
||||
this.roles = roles != null ? roles : Collections.emptySet();
|
||||
}
|
||||
|
||||
public String getDeptId() {
|
||||
public Long getDeptId() {
|
||||
return deptId;
|
||||
}
|
||||
|
||||
public void setDeptId(String deptId) {
|
||||
public void setDeptId(Long deptId) {
|
||||
this.deptId = deptId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户数据是否有效
|
||||
*
|
||||
* @return 是否有效
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return userId != null && deptId != null && !roles.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -14,17 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.extension.datapermission.filter;
|
||||
package top.continew.starter.extension.datapermission.provider;
|
||||
|
||||
import top.continew.starter.extension.datapermission.model.UserContext;
|
||||
import top.continew.starter.extension.datapermission.model.UserData;
|
||||
|
||||
/**
|
||||
* 数据权限用户上下文提供者
|
||||
* 数据权限用户数据提供者
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public interface DataPermissionUserContextProvider {
|
||||
public interface DataPermissionUserDataProvider {
|
||||
|
||||
/**
|
||||
* 是否过滤
|
||||
@@ -34,9 +34,9 @@ public interface DataPermissionUserContextProvider {
|
||||
boolean isFilter();
|
||||
|
||||
/**
|
||||
* 获取用户上下文
|
||||
* 获取用户数据
|
||||
*
|
||||
* @return 用户上下文
|
||||
* @return 用户数据
|
||||
*/
|
||||
UserContext getUserContext();
|
||||
UserData getUserData();
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.continew.starter.extension.datapermission.filter.DataPermissionUserContextProvider;
|
||||
import top.continew.starter.extension.datapermission.provider.DataPermissionUserDataProvider;
|
||||
import top.continew.starter.extension.datapermission.handler.DefaultDataPermissionHandler;
|
||||
|
||||
/**
|
||||
@@ -61,21 +61,21 @@ public class DataPermissionAutoConfiguration {
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public DataPermissionHandler dataPermissionHandler(DataPermissionUserContextProvider dataPermissionUserContextProvider) {
|
||||
return new DefaultDataPermissionHandler(dataPermissionUserContextProvider);
|
||||
public DataPermissionHandler dataPermissionHandler(DataPermissionUserDataProvider dataPermissionUserDataProvider) {
|
||||
return new DefaultDataPermissionHandler(dataPermissionUserDataProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据权限用户上下文提供者
|
||||
* 数据权限用户数据提供者
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public DataPermissionUserContextProvider dataPermissionUserContextProvider() {
|
||||
public DataPermissionUserDataProvider dataPermissionUserDataProvider() {
|
||||
if (log.isErrorEnabled()) {
|
||||
log.error("Consider defining a bean of type '{}' in your configuration.", ResolvableType
|
||||
.forClass(DataPermissionUserContextProvider.class));
|
||||
.forClass(DataPermissionUserDataProvider.class));
|
||||
}
|
||||
throw new NoSuchBeanDefinitionException(DataPermissionUserContextProvider.class);
|
||||
throw new NoSuchBeanDefinitionException(DataPermissionUserDataProvider.class);
|
||||
}
|
||||
|
||||
static {
|
||||
|
||||
@@ -46,10 +46,12 @@ import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.data.enums.DatabaseType;
|
||||
import top.continew.starter.data.util.MetaUtils;
|
||||
import top.continew.starter.extension.datapermission.annotation.DataPermission;
|
||||
import top.continew.starter.extension.datapermission.constant.DataPermissionConstants;
|
||||
import top.continew.starter.extension.datapermission.enums.DataScope;
|
||||
import top.continew.starter.extension.datapermission.filter.DataPermissionUserContextProvider;
|
||||
import top.continew.starter.extension.datapermission.model.RoleContext;
|
||||
import top.continew.starter.extension.datapermission.model.UserContext;
|
||||
import top.continew.starter.extension.datapermission.exception.DataPermissionException;
|
||||
import top.continew.starter.extension.datapermission.provider.DataPermissionUserDataProvider;
|
||||
import top.continew.starter.extension.datapermission.model.RoleData;
|
||||
import top.continew.starter.extension.datapermission.model.UserData;
|
||||
|
||||
/**
|
||||
* 默认数据权限处理器
|
||||
@@ -61,36 +63,56 @@ import top.continew.starter.extension.datapermission.model.UserContext;
|
||||
public class DefaultDataPermissionHandler implements DataPermissionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DefaultDataPermissionHandler.class);
|
||||
private final DataPermissionUserContextProvider dataPermissionUserContextProvider;
|
||||
private static final DataSource dataSource = SpringUtil.getBean(DataSource.class);
|
||||
private final DataPermissionUserDataProvider dataPermissionUserDataProvider;
|
||||
|
||||
public DefaultDataPermissionHandler(DataPermissionUserContextProvider dataPermissionUserContextProvider) {
|
||||
this.dataPermissionUserContextProvider = dataPermissionUserContextProvider;
|
||||
public DefaultDataPermissionHandler(DataPermissionUserDataProvider dataPermissionUserDataProvider) {
|
||||
this.dataPermissionUserDataProvider = dataPermissionUserDataProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getSqlSegment(Expression where, String mappedStatementId) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId
|
||||
.lastIndexOf(StringConstants.DOT)));
|
||||
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringConstants.DOT) + 1);
|
||||
Method[] methodArr = clazz.getMethods();
|
||||
for (Method method : methodArr) {
|
||||
DataPermission dataPermission = method.getAnnotation(DataPermission.class);
|
||||
String name = method.getName();
|
||||
if (dataPermission == null || !CharSequenceUtil.equalsAny(methodName, name, name + "_COUNT")) {
|
||||
continue;
|
||||
}
|
||||
if (dataPermissionUserContextProvider.isFilter()) {
|
||||
return buildDataScopeFilter(dataPermission, where);
|
||||
}
|
||||
DataPermission dataPermission = findDataPermissionAnnotation(mappedStatementId);
|
||||
if (dataPermission != null && dataPermissionUserDataProvider.isFilter()) {
|
||||
return buildDataScopeFilter(dataPermission, where);
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
} catch (Exception e) {
|
||||
log.error("Data permission handler build data scope filter occurred an error: {}.", e.getMessage(), e);
|
||||
}
|
||||
return where;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找数据权限注解
|
||||
*
|
||||
* @param mappedStatementId Mapper 方法 ID
|
||||
* @return 数据权限注解
|
||||
*/
|
||||
private DataPermission findDataPermissionAnnotation(String mappedStatementId) {
|
||||
try {
|
||||
int lastDotIndex = mappedStatementId.lastIndexOf(StringConstants.DOT);
|
||||
if (lastDotIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String className = mappedStatementId.substring(0, lastDotIndex);
|
||||
String methodName = mappedStatementId.substring(lastDotIndex + 1);
|
||||
|
||||
Class<?> clazz = Class.forName(className);
|
||||
Method[] methods = clazz.getMethods();
|
||||
|
||||
for (Method method : methods) {
|
||||
String name = method.getName();
|
||||
if (CharSequenceUtil.equalsAny(methodName, name, name + DataPermissionConstants.COUNT_METHOD_SUFFIX)) {
|
||||
return method.getAnnotation(DataPermission.class);
|
||||
}
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw DataPermissionException.methodNotFound(mappedStatementId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建数据范围过滤条件
|
||||
*
|
||||
@@ -99,23 +121,29 @@ public class DefaultDataPermissionHandler implements DataPermissionHandler {
|
||||
* @return 构建后查询条件
|
||||
*/
|
||||
private Expression buildDataScopeFilter(DataPermission dataPermission, Expression where) {
|
||||
UserData userData = dataPermissionUserDataProvider.getUserData();
|
||||
if (userData == null || !userData.isValid()) {
|
||||
throw DataPermissionException.invalidUserData("User data is null or invalid");
|
||||
}
|
||||
|
||||
Expression expression = null;
|
||||
UserContext userContext = dataPermissionUserContextProvider.getUserContext();
|
||||
Set<RoleContext> roles = userContext.getRoles();
|
||||
for (RoleContext roleContext : roles) {
|
||||
DataScope dataScope = roleContext.getDataScope();
|
||||
Set<RoleData> roles = userData.getRoles();
|
||||
|
||||
for (RoleData roleData : roles) {
|
||||
DataScope dataScope = roleData.getDataScope();
|
||||
if (DataScope.ALL.equals(dataScope)) {
|
||||
return where;
|
||||
}
|
||||
switch (dataScope) {
|
||||
case DEPT_AND_CHILD -> expression = this
|
||||
.buildDeptAndChildExpression(dataPermission, userContext, expression);
|
||||
case DEPT -> expression = this.buildDeptExpression(dataPermission, userContext, expression);
|
||||
case SELF -> expression = this.buildSelfExpression(dataPermission, userContext, expression);
|
||||
case CUSTOM -> expression = this.buildCustomExpression(dataPermission, roleContext, expression);
|
||||
default -> throw new IllegalArgumentException("暂不支持 [%s] 数据权限".formatted(dataScope));
|
||||
}
|
||||
|
||||
expression = switch (dataScope) {
|
||||
case DEPT_AND_CHILD -> buildDeptAndChildExpression(dataPermission, userData, expression);
|
||||
case DEPT -> buildDeptExpression(dataPermission, userData, expression);
|
||||
case SELF -> buildSelfExpression(dataPermission, userData, expression);
|
||||
case CUSTOM -> buildCustomExpression(dataPermission, roleData, expression);
|
||||
default -> throw DataPermissionException.unsupportedDataScope(dataScope.toString());
|
||||
};
|
||||
}
|
||||
|
||||
return where != null ? new AndExpression(where, new ParenthesedExpressionList<>(expression)) : expression;
|
||||
}
|
||||
|
||||
@@ -128,12 +156,12 @@ public class DefaultDataPermissionHandler implements DataPermissionHandler {
|
||||
* </p>
|
||||
*
|
||||
* @param dataPermission 数据权限
|
||||
* @param userContext 用户上下文
|
||||
* @param userData 用户数据
|
||||
* @param expression 处理前的表达式
|
||||
* @return 处理完后的表达式
|
||||
*/
|
||||
private Expression buildDeptAndChildExpression(DataPermission dataPermission,
|
||||
UserContext userContext,
|
||||
UserData userData,
|
||||
Expression expression) {
|
||||
ParenthesedSelect subSelect = new ParenthesedSelect();
|
||||
PlainSelect select = new PlainSelect();
|
||||
@@ -142,28 +170,29 @@ public class DefaultDataPermissionHandler implements DataPermissionHandler {
|
||||
|
||||
EqualsTo equalsTo = new EqualsTo();
|
||||
equalsTo.setLeftExpression(new Column(dataPermission.id()));
|
||||
equalsTo.setRightExpression(new LongValue(userContext.getDeptId()));
|
||||
equalsTo.setRightExpression(new LongValue(userData.getDeptId()));
|
||||
|
||||
DatabaseType databaseType = MetaUtils.getDatabaseType(dataSource);
|
||||
DatabaseType databaseType = MetaUtils.getDatabaseType(SpringUtil.getBean(DataSource.class));
|
||||
Expression inSetExpression;
|
||||
if (DatabaseType.MYSQL.getDatabase().equalsIgnoreCase(databaseType.getDatabase())) {
|
||||
Function findInSetFunction = new Function();
|
||||
findInSetFunction.setName("find_in_set");
|
||||
findInSetFunction.setParameters(new ExpressionList(new LongValue(userContext
|
||||
.getDeptId()), new Column("ancestors")));
|
||||
findInSetFunction.setParameters(new ExpressionList(new LongValue(userData
|
||||
.getDeptId()), new Column(DataPermissionConstants.ANCESTORS_COLUMN)));
|
||||
inSetExpression = findInSetFunction;
|
||||
} else if (DatabaseType.POSTGRE_SQL.getDatabase().equalsIgnoreCase(databaseType.getDatabase())) {
|
||||
// 构建 concat 函数
|
||||
Function concatFunction = new Function("concat");
|
||||
concatFunction.setParameters(new ExpressionList<>(new Column("ancestors"), new StringValue(",")));
|
||||
concatFunction
|
||||
.setParameters(new ExpressionList<>(new Column(DataPermissionConstants.ANCESTORS_COLUMN), new StringValue(",")));
|
||||
|
||||
// 创建 LIKE 函数
|
||||
LikeExpression likeExpression = new LikeExpression();
|
||||
likeExpression.setLeftExpression(concatFunction);
|
||||
likeExpression.setRightExpression(new StringValue("%," + userContext.getDeptId() + ",%"));
|
||||
likeExpression.setRightExpression(new StringValue("%," + userData.getDeptId() + ",%"));
|
||||
inSetExpression = likeExpression;
|
||||
} else {
|
||||
throw new IllegalArgumentException("暂不支持 [%s] 数据权限".formatted(""));
|
||||
throw DataPermissionException.unsupportedDatabase(databaseType.getDatabase());
|
||||
}
|
||||
|
||||
select.setWhere(new OrExpression(equalsTo, inSetExpression));
|
||||
@@ -183,16 +212,14 @@ public class DefaultDataPermissionHandler implements DataPermissionHandler {
|
||||
* </p>
|
||||
*
|
||||
* @param dataPermission 数据权限
|
||||
* @param userContext 用户上下文
|
||||
* @param userData 用户数据
|
||||
* @param expression 处理前的表达式
|
||||
* @return 处理完后的表达式
|
||||
*/
|
||||
private Expression buildDeptExpression(DataPermission dataPermission,
|
||||
UserContext userContext,
|
||||
Expression expression) {
|
||||
private Expression buildDeptExpression(DataPermission dataPermission, UserData userData, Expression expression) {
|
||||
EqualsTo equalsTo = new EqualsTo();
|
||||
equalsTo.setLeftExpression(this.buildColumn(dataPermission.tableAlias(), dataPermission.deptId()));
|
||||
equalsTo.setRightExpression(new LongValue(userContext.getDeptId()));
|
||||
equalsTo.setRightExpression(new LongValue(userData.getDeptId()));
|
||||
return expression != null ? new OrExpression(expression, equalsTo) : equalsTo;
|
||||
}
|
||||
|
||||
@@ -204,16 +231,14 @@ public class DefaultDataPermissionHandler implements DataPermissionHandler {
|
||||
* </p>
|
||||
*
|
||||
* @param dataPermission 数据权限
|
||||
* @param userContext 用户上下文
|
||||
* @param userData 用户数据
|
||||
* @param expression 处理前的表达式
|
||||
* @return 处理完后的表达式
|
||||
*/
|
||||
private Expression buildSelfExpression(DataPermission dataPermission,
|
||||
UserContext userContext,
|
||||
Expression expression) {
|
||||
private Expression buildSelfExpression(DataPermission dataPermission, UserData userData, Expression expression) {
|
||||
EqualsTo equalsTo = new EqualsTo();
|
||||
equalsTo.setLeftExpression(this.buildColumn(dataPermission.tableAlias(), dataPermission.userId()));
|
||||
equalsTo.setRightExpression(new LongValue(userContext.getUserId()));
|
||||
equalsTo.setRightExpression(new LongValue(userData.getUserId()));
|
||||
return expression != null ? new OrExpression(expression, equalsTo) : equalsTo;
|
||||
}
|
||||
|
||||
@@ -226,20 +251,18 @@ public class DefaultDataPermissionHandler implements DataPermissionHandler {
|
||||
* </p>
|
||||
*
|
||||
* @param dataPermission 数据权限
|
||||
* @param roleContext 角色上下文
|
||||
* @param roleData 角色上下文
|
||||
* @param expression 处理前的表达式
|
||||
* @return 处理完后的表达式
|
||||
*/
|
||||
private Expression buildCustomExpression(DataPermission dataPermission,
|
||||
RoleContext roleContext,
|
||||
Expression expression) {
|
||||
private Expression buildCustomExpression(DataPermission dataPermission, RoleData roleData, Expression expression) {
|
||||
ParenthesedSelect subSelect = new ParenthesedSelect();
|
||||
PlainSelect select = new PlainSelect();
|
||||
select.setSelectItems(Collections.singletonList(new SelectItem<>(new Column(dataPermission.deptId()))));
|
||||
select.setFromItem(new Table(dataPermission.roleDeptTableAlias()));
|
||||
EqualsTo equalsTo = new EqualsTo();
|
||||
equalsTo.setLeftExpression(new Column(dataPermission.roleId()));
|
||||
equalsTo.setRightExpression(new LongValue(roleContext.getRoleId()));
|
||||
equalsTo.setRightExpression(new LongValue(roleData.getRoleId()));
|
||||
select.setWhere(equalsTo);
|
||||
subSelect.setSelect(select);
|
||||
// 构建父查询
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>ContiNew Starter 扩展模块 - 多租户 - 核心模块</description>
|
||||
<description>ContiNew Starter 扩展模块 - 租户 - 核心模块</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- TTL(线程间传递 ThreadLocal,异步执行时上下文传递的解决方案) -->
|
||||
|
||||
@@ -57,4 +57,9 @@ public interface TenantDataSourceHandler {
|
||||
* @param dataSourceName 数据源名称
|
||||
*/
|
||||
void removeDataSource(String dataSourceName);
|
||||
|
||||
/**
|
||||
* 轮询数据源
|
||||
*/
|
||||
void poll();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.extension.tenant.annotation;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 是否启用租户判断注解
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.13.1
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Documented
|
||||
@ConditionalOnProperty(prefix = PropertiesConstants.TENANT, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
|
||||
public @interface ConditionalOnEnabledTenant {
|
||||
}
|
||||
@@ -19,7 +19,11 @@ package top.continew.starter.extension.tenant.annotation;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 多租户忽略注解
|
||||
* 租户忽略注解
|
||||
*
|
||||
* <p>
|
||||
* 例如:定时任务等需要忽略租户的场景
|
||||
* </p>
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.7.0
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.extension.tenant.aop;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import top.continew.starter.extension.tenant.annotation.TenantIgnore;
|
||||
import top.continew.starter.extension.tenant.context.TenantContextHolder;
|
||||
|
||||
/**
|
||||
* 租户忽略注解切面
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.13.1
|
||||
*/
|
||||
@Aspect
|
||||
public class TenantIgnoreAspect {
|
||||
|
||||
/**
|
||||
* 忽略租户
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @return 返回结果
|
||||
* @throws Throwable 异常
|
||||
*/
|
||||
@Around("@annotation(tenantIgnore)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable {
|
||||
boolean oldIgnore = TenantContextHolder.isIgnore();
|
||||
if (oldIgnore) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
try {
|
||||
TenantContextHolder.setIgnore(true);
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
TenantContextHolder.setIgnore(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,12 +52,7 @@ public class TenantProperties {
|
||||
private String tenantIdHeader = "X-Tenant-Id";
|
||||
|
||||
/**
|
||||
* 超级租户 ID
|
||||
*/
|
||||
private Long superTenantId = 1L;
|
||||
|
||||
/**
|
||||
* 忽略表(忽略拼接多租户条件)
|
||||
* 忽略表(忽略拼接租户条件)
|
||||
*/
|
||||
private List<String> ignoreTables;
|
||||
|
||||
@@ -93,14 +88,6 @@ public class TenantProperties {
|
||||
this.tenantIdHeader = tenantIdHeader;
|
||||
}
|
||||
|
||||
public Long getSuperTenantId() {
|
||||
return superTenantId;
|
||||
}
|
||||
|
||||
public void setSuperTenantId(Long superTenantId) {
|
||||
this.superTenantId = superTenantId;
|
||||
}
|
||||
|
||||
public List<String> getIgnoreTables() {
|
||||
return ignoreTables;
|
||||
}
|
||||
|
||||
@@ -17,22 +17,25 @@
|
||||
package top.continew.starter.extension.tenant.autoconfigure;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.continew.starter.extension.tenant.annotation.ConditionalOnEnabledTenant;
|
||||
import top.continew.starter.extension.tenant.config.TenantProvider;
|
||||
import top.continew.starter.extension.tenant.interceptor.TenantInterceptor;
|
||||
|
||||
/**
|
||||
* 多租户 Web MVC 自动配置
|
||||
* 租户 Web MVC 自动配置
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.7.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnEnabledTenant
|
||||
@ConditionalOnWebApplication
|
||||
@ConditionalOnProperty(prefix = PropertiesConstants.TENANT, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
|
||||
@EnableConfigurationProperties(TenantProperties.class)
|
||||
public class TenantWebMvcAutoConfiguration implements WebMvcConfigurer {
|
||||
|
||||
private final TenantProperties tenantProperties;
|
||||
@@ -45,6 +48,7 @@ public class TenantWebMvcAutoConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new TenantInterceptor(tenantProperties, tenantProvider));
|
||||
registry.addInterceptor(new TenantInterceptor(tenantProperties, tenantProvider))
|
||||
.order(Ordered.HIGHEST_PRECEDENCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@ public interface TenantProvider {
|
||||
/**
|
||||
* 根据租户 ID 获取租户上下文
|
||||
*
|
||||
* @param tenantId 租户 ID
|
||||
* @param isVerify 是否验证有效性
|
||||
* @param tenantIdAsString 租户 ID 字符串
|
||||
* @param isVerify 是否验证有效性
|
||||
* @return 租户上下文
|
||||
*/
|
||||
TenantContext getByTenantId(String tenantId, boolean isVerify);
|
||||
TenantContext getByTenantId(String tenantIdAsString, boolean isVerify);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public class TenantContext {
|
||||
/**
|
||||
* 隔离级别
|
||||
*/
|
||||
private TenantIsolationLevel isolationLevel;
|
||||
private TenantIsolationLevel isolationLevel = TenantIsolationLevel.LINE;
|
||||
|
||||
/**
|
||||
* 数据源信息
|
||||
|
||||
@@ -18,6 +18,7 @@ package top.continew.starter.extension.tenant.context;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||
import top.continew.starter.core.util.SpringUtils;
|
||||
import top.continew.starter.extension.tenant.autoconfigure.TenantProperties;
|
||||
import top.continew.starter.extension.tenant.config.TenantDataSource;
|
||||
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
|
||||
@@ -32,8 +33,16 @@ import java.util.Optional;
|
||||
*/
|
||||
public class TenantContextHolder {
|
||||
|
||||
/**
|
||||
* 租户上下文
|
||||
*/
|
||||
private static final TransmittableThreadLocal<TenantContext> CONTEXT_HOLDER = new TransmittableThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 是否忽略租户
|
||||
*/
|
||||
private static final TransmittableThreadLocal<Boolean> IGNORE_HOLDER = new TransmittableThreadLocal<>();
|
||||
|
||||
private TenantContextHolder() {
|
||||
}
|
||||
|
||||
@@ -56,10 +65,29 @@ public class TenantContextHolder {
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除上下文
|
||||
* 设置是否忽略租户
|
||||
*
|
||||
* @param ignore 是否忽略租户
|
||||
*/
|
||||
public static void clearContext() {
|
||||
public static void setIgnore(boolean ignore) {
|
||||
IGNORE_HOLDER.set(ignore);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否忽略租户
|
||||
*
|
||||
* @return 是否忽略租户
|
||||
*/
|
||||
public static boolean isIgnore() {
|
||||
return Boolean.TRUE.equals(IGNORE_HOLDER.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除
|
||||
*/
|
||||
public static void clear() {
|
||||
CONTEXT_HOLDER.remove();
|
||||
IGNORE_HOLDER.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,4 +118,23 @@ public class TenantContextHolder {
|
||||
public static TenantDataSource getDataSource() {
|
||||
return Optional.ofNullable(getContext()).map(TenantContext::getDataSource).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用了租户
|
||||
*
|
||||
* @return 是否启用了租户
|
||||
*/
|
||||
public static boolean isTenantEnabled() {
|
||||
TenantProperties tenantProperties = SpringUtils.getBean(TenantProperties.class, true);
|
||||
return tenantProperties != null && tenantProperties.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否禁用了租户
|
||||
*
|
||||
* @return 是否禁用了租户
|
||||
*/
|
||||
public static boolean isTenantDisabled() {
|
||||
return !isTenantEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.extension.tenant.autoconfigure;
|
||||
package top.continew.starter.extension.tenant.interceptor;
|
||||
|
||||
import cn.hutool.core.annotation.AnnotationUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import top.continew.starter.extension.tenant.annotation.TenantIgnore;
|
||||
import top.continew.starter.extension.tenant.autoconfigure.TenantProperties;
|
||||
import top.continew.starter.extension.tenant.config.TenantProvider;
|
||||
import top.continew.starter.extension.tenant.context.TenantContextHolder;
|
||||
|
||||
@@ -31,7 +32,7 @@ import top.continew.starter.extension.tenant.context.TenantContextHolder;
|
||||
* @author Charles7c
|
||||
* @since 2.7.0
|
||||
*/
|
||||
public class TenantInterceptor implements HandlerInterceptor, Ordered {
|
||||
public class TenantInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final TenantProperties tenantProperties;
|
||||
private final TenantProvider tenantProvider;
|
||||
@@ -43,19 +44,34 @@ public class TenantInterceptor implements HandlerInterceptor, Ordered {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
if (handler instanceof HandlerMethod handlerMethod) {
|
||||
TenantIgnore tenantIgnore = handlerMethod.getMethodAnnotation(TenantIgnore.class);
|
||||
if (tenantIgnore != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 设置上下文
|
||||
String tenantId = request.getHeader(tenantProperties.getTenantIdHeader());
|
||||
TenantContextHolder.setContext(tenantProvider.getByTenantId(tenantId, true));
|
||||
// 设置是否忽略租户
|
||||
TenantContextHolder.setIgnore(this.isIgnore(handler));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Integer.MIN_VALUE;
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
|
||||
// 清除上下文
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否忽略租户
|
||||
*
|
||||
* @param handler 处理器
|
||||
* @return 是否忽略租户
|
||||
*/
|
||||
private boolean isIgnore(Object handler) {
|
||||
if (handler instanceof HandlerMethod handlerMethod) {
|
||||
TenantIgnore methodAnnotation = handlerMethod.getMethodAnnotation(TenantIgnore.class);
|
||||
if (methodAnnotation != null) {
|
||||
return true;
|
||||
}
|
||||
return AnnotationUtil.getAnnotation(handlerMethod.getBeanType(), TenantIgnore.class) != null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.extension.tenant.util;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import top.continew.starter.extension.tenant.TenantDataSourceHandler;
|
||||
import top.continew.starter.extension.tenant.config.TenantProvider;
|
||||
import top.continew.starter.extension.tenant.context.TenantContext;
|
||||
import top.continew.starter.extension.tenant.context.TenantContextHolder;
|
||||
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
|
||||
|
||||
/**
|
||||
* 租户工具类
|
||||
*
|
||||
* @author 小熊
|
||||
* @author Charles7c
|
||||
* @since 2.13.1
|
||||
*/
|
||||
public class TenantUtils {
|
||||
|
||||
private TenantUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定租户执行业务逻辑
|
||||
*
|
||||
* <p>
|
||||
* 强制设置 {@code TenantContextHolder.setIgnore(false)},执行完恢复原值。<br>
|
||||
* 适用于在非租户逻辑中执行有租户逻辑的业务逻辑。
|
||||
* </p>
|
||||
*
|
||||
* @param tenantId 租户 ID
|
||||
* @param runnable 业务逻辑
|
||||
*/
|
||||
public static void execute(Long tenantId, Runnable runnable) {
|
||||
// 未启用租户,直接执行业务逻辑
|
||||
if (TenantContextHolder.isTenantDisabled()) {
|
||||
runnable.run();
|
||||
return;
|
||||
}
|
||||
// 原租户上下文
|
||||
TenantContext oldContext = TenantContextHolder.getContext();
|
||||
boolean oldIgnore = TenantContextHolder.isIgnore();
|
||||
boolean isPush = false;
|
||||
try {
|
||||
TenantContext newContext = SpringUtil.getBean(TenantProvider.class)
|
||||
.getByTenantId(tenantId.toString(), false);
|
||||
// 设置新租户上下文
|
||||
TenantContextHolder.setContext(newContext);
|
||||
TenantContextHolder.setIgnore(false);
|
||||
// 数据源级隔离:切换数据源
|
||||
if (TenantIsolationLevel.DATASOURCE.equals(newContext.getIsolationLevel())) {
|
||||
SpringUtil.getBean(TenantDataSourceHandler.class).changeDataSource(newContext.getDataSource());
|
||||
isPush = true;
|
||||
}
|
||||
// 执行业务逻辑
|
||||
runnable.run();
|
||||
} finally {
|
||||
// 恢复原租户上下文
|
||||
TenantContextHolder.setContext(oldContext);
|
||||
TenantContextHolder.setIgnore(oldIgnore);
|
||||
if (isPush) {
|
||||
SpringUtil.getBean(TenantDataSourceHandler.class).poll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 忽略租户执行业务逻辑
|
||||
*
|
||||
* <p>
|
||||
* 适用于在租户逻辑中执行非租户逻辑的业务逻辑。
|
||||
* </p>
|
||||
*
|
||||
* @param runnable 业务逻辑
|
||||
*/
|
||||
public static void executeIgnore(Runnable runnable) {
|
||||
// 未启用或忽略租户,直接执行业务逻辑
|
||||
if (TenantContextHolder.isTenantDisabled() || TenantContextHolder.isIgnore()) {
|
||||
runnable.run();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
TenantContextHolder.setIgnore(true);
|
||||
// 执行业务逻辑
|
||||
runnable.run();
|
||||
} finally {
|
||||
TenantContextHolder.setIgnore(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,10 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>ContiNew Starter 扩展模块 - 多租户 - MyBatis Plus ORM 模块</description>
|
||||
<description>ContiNew Starter 扩展模块 - 租户 - MyBatis Plus ORM 模块</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- 多租户 - 核心模块 -->
|
||||
<!-- 租户 - 核心模块 -->
|
||||
<dependency>
|
||||
<groupId>top.continew.starter</groupId>
|
||||
<artifactId>continew-starter-extension-tenant-core</artifactId>
|
||||
@@ -32,6 +32,7 @@
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package top.continew.starter.extension.tenant.autoconfigure;
|
||||
|
||||
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
|
||||
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
@@ -25,16 +23,14 @@ 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.ConditionalOnClass;
|
||||
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.core.constant.PropertiesConstants;
|
||||
import top.continew.starter.extension.tenant.annotation.ConditionalOnEnabledTenant;
|
||||
import top.continew.starter.extension.tenant.aop.TenantIgnoreAspect;
|
||||
import top.continew.starter.extension.tenant.config.TenantProvider;
|
||||
import top.continew.starter.extension.tenant.handler.DefaultTenantHandler;
|
||||
import top.continew.starter.extension.tenant.TenantDataSourceHandler;
|
||||
import top.continew.starter.extension.tenant.TenantHandler;
|
||||
import top.continew.starter.extension.tenant.handler.datasource.DefaultTenantDataSourceHandler;
|
||||
import top.continew.starter.extension.tenant.handler.datasource.TenantDataSourceAdvisor;
|
||||
import top.continew.starter.extension.tenant.handler.datasource.TenantDataSourceInterceptor;
|
||||
@@ -49,8 +45,7 @@ import javax.sql.DataSource;
|
||||
* @since 2.7.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(TenantProperties.class)
|
||||
@ConditionalOnProperty(prefix = PropertiesConstants.TENANT, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
|
||||
@ConditionalOnEnabledTenant
|
||||
public class TenantAutoConfiguration {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TenantAutoConfiguration.class);
|
||||
@@ -60,6 +55,15 @@ public class TenantAutoConfiguration {
|
||||
this.tenantProperties = tenantProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户忽略切面
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public TenantIgnoreAspect tenantIgnoreAspect() {
|
||||
return new TenantIgnoreAspect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户行级隔离拦截器
|
||||
*/
|
||||
@@ -83,6 +87,7 @@ public class TenantAutoConfiguration {
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnClass(name = "com.baomidou.dynamic.datasource.DynamicRoutingDataSource")
|
||||
public TenantDataSourceAdvisor tenantDataSourceAdvisor(TenantDataSourceInterceptor tenantDataSourceInterceptor) {
|
||||
return new TenantDataSourceAdvisor(tenantDataSourceInterceptor);
|
||||
}
|
||||
@@ -92,6 +97,7 @@ public class TenantAutoConfiguration {
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnClass(name = "com.baomidou.dynamic.datasource.DynamicRoutingDataSource")
|
||||
public TenantDataSourceInterceptor tenantDataSourceInterceptor(TenantDataSourceHandler tenantDataSourceHandler) {
|
||||
return new TenantDataSourceInterceptor(tenantDataSourceHandler);
|
||||
}
|
||||
@@ -101,9 +107,9 @@ public class TenantAutoConfiguration {
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public TenantDataSourceHandler tenantDataSourceHandler(DataSource dataSource,
|
||||
DefaultDataSourceCreator dataSourceCreator) {
|
||||
return new DefaultTenantDataSourceHandler((DynamicRoutingDataSource)dataSource, dataSourceCreator);
|
||||
@ConditionalOnClass(name = "com.baomidou.dynamic.datasource.DynamicRoutingDataSource")
|
||||
public TenantDataSourceHandler tenantDataSourceHandler(DataSource dataSource) {
|
||||
return new DefaultTenantDataSourceHandler(dataSource);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,15 +125,6 @@ public class TenantAutoConfiguration {
|
||||
throw new NoSuchBeanDefinitionException(TenantProvider.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户处理器
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public TenantHandler tenantHandler(TenantDataSourceHandler tenantDataSourceHandler, TenantProvider tenantProvider) {
|
||||
return new DefaultTenantHandler(tenantProperties, tenantDataSourceHandler, tenantProvider);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct() {
|
||||
log.debug("[ContiNew Starter] - Auto Configuration 'Tenant' completed initialization.");
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* 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.extension.tenant.handler;
|
||||
|
||||
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
|
||||
import top.continew.starter.extension.tenant.TenantDataSourceHandler;
|
||||
import top.continew.starter.extension.tenant.TenantHandler;
|
||||
import top.continew.starter.extension.tenant.autoconfigure.TenantProperties;
|
||||
import top.continew.starter.extension.tenant.config.TenantProvider;
|
||||
import top.continew.starter.extension.tenant.context.TenantContext;
|
||||
import top.continew.starter.extension.tenant.context.TenantContextHolder;
|
||||
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
|
||||
|
||||
/**
|
||||
* 租户处理器
|
||||
*
|
||||
* @author 小熊
|
||||
* @since 2.8.0
|
||||
*/
|
||||
public class DefaultTenantHandler implements TenantHandler {
|
||||
|
||||
private final TenantProperties tenantProperties;
|
||||
private final TenantDataSourceHandler dataSourceHandler;
|
||||
private final TenantProvider tenantProvider;
|
||||
|
||||
public DefaultTenantHandler(TenantProperties tenantProperties,
|
||||
TenantDataSourceHandler dataSourceHandler,
|
||||
TenantProvider tenantProvider) {
|
||||
this.tenantProperties = tenantProperties;
|
||||
this.dataSourceHandler = dataSourceHandler;
|
||||
this.tenantProvider = tenantProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Long tenantId, Runnable runnable) {
|
||||
if (!tenantProperties.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
TenantContext tenantHandler = tenantProvider.getByTenantId(tenantId.toString(), false);
|
||||
// 保存当前的租户上下文
|
||||
TenantContext originalContext = TenantContextHolder.getContext();
|
||||
boolean isPush = false;
|
||||
try {
|
||||
// 设置新的租户上下文
|
||||
TenantContextHolder.setContext(tenantHandler);
|
||||
// 切换数据源
|
||||
if (TenantIsolationLevel.DATASOURCE.equals(tenantHandler.getIsolationLevel())) {
|
||||
dataSourceHandler.changeDataSource(tenantHandler.getDataSource());
|
||||
isPush = true;
|
||||
}
|
||||
// 执行业务逻辑
|
||||
runnable.run();
|
||||
} finally {
|
||||
// 恢复原始的租户上下文
|
||||
if (originalContext != null) {
|
||||
TenantContextHolder.setContext(originalContext);
|
||||
} else {
|
||||
TenantContextHolder.clearContext();
|
||||
}
|
||||
if (isPush) {
|
||||
DynamicDataSourceContextHolder.poll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package top.continew.starter.extension.tenant.handler.datasource;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
|
||||
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
|
||||
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
|
||||
@@ -40,10 +41,9 @@ public class DefaultTenantDataSourceHandler implements TenantDataSourceHandler {
|
||||
private final DynamicRoutingDataSource dynamicRoutingDataSource;
|
||||
private final DefaultDataSourceCreator dataSourceCreator;
|
||||
|
||||
public DefaultTenantDataSourceHandler(DynamicRoutingDataSource dynamicRoutingDataSource,
|
||||
DefaultDataSourceCreator dataSourceCreator) {
|
||||
this.dynamicRoutingDataSource = dynamicRoutingDataSource;
|
||||
this.dataSourceCreator = dataSourceCreator;
|
||||
public DefaultTenantDataSourceHandler(DataSource dataSource) {
|
||||
this.dynamicRoutingDataSource = (DynamicRoutingDataSource)dataSource;
|
||||
this.dataSourceCreator = SpringUtil.getBean(DefaultDataSourceCreator.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,4 +82,9 @@ public class DefaultTenantDataSourceHandler implements TenantDataSourceHandler {
|
||||
public void removeDataSource(String dataSourceName) {
|
||||
dynamicRoutingDataSource.removeDataSource(dataSourceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void poll() {
|
||||
DynamicDataSourceContextHolder.poll();
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,6 @@ public class TenantDataSourceAdvisor extends AbstractPointcutAdvisor implements
|
||||
AspectJExpressionPointcut cut = new AspectJExpressionPointcut();
|
||||
cut.setExpression("""
|
||||
execution(* *..controller..*(..))
|
||||
&& !@annotation(top.continew.starter.extension.tenant.annotation.TenantIgnore)
|
||||
""");
|
||||
return new ComposablePointcut((Pointcut)cut);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,11 @@ public class TenantDataSourceInterceptor implements MethodInterceptor {
|
||||
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
// 忽略租户
|
||||
if (TenantContextHolder.isIgnore()) {
|
||||
return true;
|
||||
}
|
||||
// 忽略行级隔离
|
||||
if (TenantIsolationLevel.LINE.equals(TenantContextHolder.getIsolationLevel())) {
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ import cn.hutool.core.collection.CollUtil;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import net.sf.jsqlparser.expression.NullValue;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import top.continew.starter.extension.tenant.autoconfigure.TenantProperties;
|
||||
import top.continew.starter.extension.tenant.context.TenantContextHolder;
|
||||
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
|
||||
@@ -32,6 +35,7 @@ import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
|
||||
*/
|
||||
public class DefaultTenantLineHandler implements TenantLineHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DefaultTenantLineHandler.class);
|
||||
private final TenantProperties tenantProperties;
|
||||
|
||||
public DefaultTenantLineHandler(TenantProperties tenantProperties) {
|
||||
@@ -44,7 +48,8 @@ public class DefaultTenantLineHandler implements TenantLineHandler {
|
||||
if (tenantId != null) {
|
||||
return new LongValue(tenantId);
|
||||
}
|
||||
return null;
|
||||
log.warn("Tenant ID not found in current context.");
|
||||
return new NullValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,13 +59,15 @@ public class DefaultTenantLineHandler implements TenantLineHandler {
|
||||
|
||||
@Override
|
||||
public boolean ignoreTable(String tableName) {
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId != null && tenantId.equals(tenantProperties.getSuperTenantId())) {
|
||||
// 忽略租户
|
||||
if (TenantContextHolder.isIgnore()) {
|
||||
return true;
|
||||
}
|
||||
// 忽略数据源级隔离
|
||||
if (TenantIsolationLevel.DATASOURCE.equals(TenantContextHolder.getIsolationLevel())) {
|
||||
return true;
|
||||
}
|
||||
// 忽略指定表
|
||||
return CollUtil.contains(tenantProperties.getIgnoreTables(), tableName);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>ContiNew Starter 扩展模块 - 多租户</description>
|
||||
<description>ContiNew Starter 扩展模块 - 租户</description>
|
||||
|
||||
<modules>
|
||||
<module>continew-starter-extension-tenant-core</module>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.json.jackson.exception;
|
||||
|
||||
import top.continew.starter.core.exception.BaseException;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* JSON 异常
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.13.2
|
||||
*/
|
||||
public class JSONException extends BaseException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public JSONException() {
|
||||
}
|
||||
|
||||
public JSONException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public JSONException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public JSONException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,9 @@ import java.util.Objects;
|
||||
/**
|
||||
* 枚举接口 BaseEnum 反序列化器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @see BaseEnum
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@JacksonStdImpl
|
||||
|
||||
@@ -27,8 +27,9 @@ import java.io.IOException;
|
||||
/**
|
||||
* 枚举接口 BaseEnum 序列化器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @see BaseEnum
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@JacksonStdImpl
|
||||
|
||||
@@ -27,7 +27,9 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* json 构建工具
|
||||
* JSON 构建工具
|
||||
*
|
||||
* @see ObjectMapper
|
||||
*
|
||||
* @author echo
|
||||
* @since 2.11.0
|
||||
|
||||
@@ -16,36 +16,36 @@
|
||||
|
||||
package top.continew.starter.json.jackson.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import top.continew.starter.json.jackson.exception.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* json 工具
|
||||
* JSON 工具类
|
||||
*
|
||||
* @see ObjectMapper
|
||||
* @see cn.hutool.json.JSONUtil
|
||||
*
|
||||
* @author echo
|
||||
* @author Charles7c
|
||||
* @since 2.11.0
|
||||
*/
|
||||
public class JSONUtils {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = SpringUtil.getBean(ObjectMapper.class);
|
||||
|
||||
private JSONUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Jackson 对象映射器,用于 JSON 解析与序列化。
|
||||
*/
|
||||
private static final ObjectMapper OBJECT_MAPPER = SpringUtil.getBean(ObjectMapper.class);
|
||||
|
||||
/**
|
||||
* 获取 Jackson 对象映射器。
|
||||
* 获取 Jackson 对象映射器
|
||||
*
|
||||
* @return {@link ObjectMapper} Jackson 对象映射器
|
||||
*/
|
||||
@@ -54,183 +54,136 @@ public class JSONUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象转为 json 字符串
|
||||
* 转换对象为JsonNode<br>
|
||||
*
|
||||
* @param object 对象
|
||||
* @return {@link String }
|
||||
* @param obj 对象
|
||||
* @return JsonNode
|
||||
*/
|
||||
public static String toJsonStr(Object object) {
|
||||
if (object == null) {
|
||||
public static JsonNode parseObj(Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.writeValueAsString(object);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return OBJECT_MAPPER.valueToTree(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为 JsonNode。
|
||||
* 转换为JSON字符串
|
||||
*
|
||||
* @param obj 需要转换的对象
|
||||
* @return 转换后的 {@link JsonNode},如果 obj 为空,则返回 null
|
||||
* @param obj 被转为JSON的对象
|
||||
* @return JSON字符串
|
||||
*/
|
||||
public static JsonNode toJson(Object obj) {
|
||||
public static String toJsonStr(Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.valueToTree(obj);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 List 转换为 JsonNode。
|
||||
*
|
||||
* @param list 输入的 List
|
||||
* @return 转换后的 {@link JsonNode}
|
||||
*/
|
||||
public static JsonNode listToJson(List<?> list) {
|
||||
return toJson(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Map 转换为 JsonNode。
|
||||
*
|
||||
* @param map 输入的 Map
|
||||
* @return 转换后的 {@link JsonNode}
|
||||
*/
|
||||
public static JsonNode mapToJson(Map<?, ?> map) {
|
||||
return toJson(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JsonNode 转换为 List<String>,用于环境变量格式解析。
|
||||
*
|
||||
* @param jsonNode 需要转换的 JsonNode
|
||||
* @return 转换后的 List<String>
|
||||
*/
|
||||
public static List<String> jsonToEnvList(JsonNode jsonNode) {
|
||||
if (jsonNode == null || jsonNode.isNull()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<String> envList = new ArrayList<>();
|
||||
jsonNode.fields().forEachRemaining(field -> {
|
||||
String key = field.getKey();
|
||||
JsonNode valueNode = field.getValue();
|
||||
String value = valueNode.isValueNode() ? valueNode.asText() : valueNode.toString();
|
||||
envList.add(key + "=" + value);
|
||||
});
|
||||
return envList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JsonNode 转换为 List<String>。
|
||||
*
|
||||
* @param jsonNode 需要转换的 JsonNode
|
||||
* @return 转换后的 List<String>
|
||||
*/
|
||||
public static List<String> jsonToStringList(JsonNode jsonNode) {
|
||||
if (jsonNode == null || jsonNode.isNull()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.convertValue(jsonNode, new TypeReference<>() {
|
||||
});
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JsonNode 转换为指定类型的 Java 对象。
|
||||
*
|
||||
* @param jsonNode JSON 数据
|
||||
* @param clazz 目标 Java 类
|
||||
* @return 解析后的 Java 对象
|
||||
*/
|
||||
public static <T> T fromJson(JsonNode jsonNode, Class<T> clazz) {
|
||||
try {
|
||||
return OBJECT_MAPPER.treeToValue(jsonNode, clazz);
|
||||
return OBJECT_MAPPER.writeValueAsString(obj);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
throw new JSONException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JSON 字符串为 Java 对象。
|
||||
* JSON字符串转为实体类对象,转换异常将被抛出
|
||||
*
|
||||
* @param str JSON 字符串
|
||||
* @param clazz 目标 Java 类
|
||||
* @return 解析后的 Java 对象
|
||||
* @param <T> Bean类型
|
||||
* @param jsonString JSON字符串
|
||||
* @param beanClass 实体类对象
|
||||
* @return 实体类对象
|
||||
*/
|
||||
public static <T> T parseObject(String str, Class<T> clazz) {
|
||||
if (StrUtil.isEmpty(str)) {
|
||||
public static <T> T toBean(String jsonString, Class<T> beanClass) {
|
||||
if (CharSequenceUtil.isBlank(jsonString)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(str, clazz);
|
||||
return OBJECT_MAPPER.readValue(jsonString, beanClass);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
throw new JSONException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串 解析为 list<T>
|
||||
* 将JSON字符串转换为Bean的List,默认为ArrayList
|
||||
*
|
||||
* @param str 字符串
|
||||
* @param clazz 目标 Java 类
|
||||
* @return 解析后的 List<T>
|
||||
* @param jsonStr 需要转换的JSON字符串
|
||||
* @param elementType 列表元素类型
|
||||
* @return 转换后的 List
|
||||
* @since 2.13.2
|
||||
*/
|
||||
public static <T> List<T> parseArray(String str, Class<T> clazz) {
|
||||
if (StrUtil.isEmpty(str)) {
|
||||
return new ArrayList<>();
|
||||
public static <T> List<T> toList(String jsonStr, Class<T> elementType) {
|
||||
if (jsonStr == null || jsonStr.trim().isEmpty()) {
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(str, OBJECT_MAPPER.getTypeFactory()
|
||||
.constructCollectionType(List.class, clazz));
|
||||
return OBJECT_MAPPER.readValue(jsonStr, OBJECT_MAPPER.getTypeFactory()
|
||||
.constructCollectionType(List.class, elementType));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
throw new JSONException("Failed to parse JSON string to list", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否为 JSON 格式。
|
||||
* 将JSONArray转换为Bean的List,默认为ArrayList
|
||||
*
|
||||
* @param jsonNode 需要转换的 JsonNode
|
||||
* @param elementType 列表元素类型
|
||||
* @return 转换后的 List
|
||||
* @since 2.13.2
|
||||
*/
|
||||
public static <T> List<T> toList(JsonNode jsonNode, Class<T> elementType) {
|
||||
if (jsonNode == null || jsonNode.isNull()) {
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.convertValue(jsonNode, OBJECT_MAPPER.getTypeFactory()
|
||||
.constructCollectionType(List.class, elementType));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new JSONException("Failed to convert JSON to list", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为JSON类型字符串,首尾都为大括号或中括号判定为JSON字符串
|
||||
*
|
||||
* @param str 字符串
|
||||
* @return 是否为 JSON 格式
|
||||
* @return 是否为JSON类型字符串
|
||||
* @since 2.13.2
|
||||
* @see cn.hutool.json.JSONUtil#isTypeJSON(String)
|
||||
* @author Looly(<a href="https://gitee.com/dromara/hutool">Hutool</a>)
|
||||
*/
|
||||
public static boolean isTypeJSON(String str) {
|
||||
if (StrUtil.isEmpty(str)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
OBJECT_MAPPER.readTree(str);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
return isTypeJSONObject(str) || isTypeJSONArray(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串转换为指定类型的 Java 对象。
|
||||
* 是否为JSONObject类型字符串,首尾都为大括号判定为JSONObject字符串
|
||||
*
|
||||
* @param str 字符串
|
||||
* @param clazz 目标对象的 Class 类型
|
||||
* @return 解析后的 Java 对象
|
||||
* @param str 字符串
|
||||
* @return 是否为JSON字符串
|
||||
* @since 2.13.2
|
||||
* @see cn.hutool.json.JSONUtil#isTypeJSONObject(String)
|
||||
* @author Looly(<a href="https://gitee.com/dromara/hutool">Hutool</a>)
|
||||
*/
|
||||
public static <T> T toBean(String str, Class<T> clazz) {
|
||||
if (StrUtil.isEmpty(str)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(str, clazz);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
public static boolean isTypeJSONObject(String str) {
|
||||
if (CharSequenceUtil.isBlank(str)) {
|
||||
return false;
|
||||
}
|
||||
return CharSequenceUtil.isWrap(CharSequenceUtil.trim(str), '{', '}');
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为JSONArray类型的字符串,首尾都为中括号判定为JSONArray字符串
|
||||
*
|
||||
* @param str 字符串
|
||||
* @return 是否为JSONArray类型字符串
|
||||
* @since 2.13.2
|
||||
* @see cn.hutool.json.JSONUtil#isTypeJSONArray(String)
|
||||
* @author Looly(<a href="https://gitee.com/dromara/hutool">Hutool</a>)
|
||||
*/
|
||||
public static boolean isTypeJSONArray(String str) {
|
||||
if (CharSequenceUtil.isBlank(str)) {
|
||||
return false;
|
||||
}
|
||||
return CharSequenceUtil.isWrap(CharSequenceUtil.trim(str), '[', ']');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.util.CollUtils;
|
||||
import top.continew.starter.license.exception.LicenseException;
|
||||
import top.continew.starter.license.model.LicenseExtraModel;
|
||||
|
||||
@@ -33,7 +34,6 @@ import java.net.SocketException;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 服务器信息工具类
|
||||
@@ -128,7 +128,7 @@ public class ServerInfoUtils {
|
||||
* @return {@link String}
|
||||
*/
|
||||
private static String getLinuxCpuSerial() {
|
||||
String result = StrUtil.EMPTY;
|
||||
String result = StringConstants.EMPTY;
|
||||
String cpuIdCmd = "dmidecode";
|
||||
BufferedReader bufferedReader = null;
|
||||
try {
|
||||
@@ -160,8 +160,7 @@ public class ServerInfoUtils {
|
||||
* @return {@link String}
|
||||
*/
|
||||
private static String getWindowCpuSerial() {
|
||||
|
||||
StringBuilder result = new StringBuilder(StrUtil.EMPTY);
|
||||
StringBuilder result = new StringBuilder(StringConstants.EMPTY);
|
||||
File file = null;
|
||||
BufferedReader input = null;
|
||||
try {
|
||||
@@ -205,11 +204,11 @@ public class ServerInfoUtils {
|
||||
try {
|
||||
Process process = new ProcessBuilder("sh", "-c", command).start();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||
return reader.lines().findFirst().orElse(StrUtil.EMPTY);
|
||||
return reader.lines().findFirst().orElse(StringConstants.EMPTY);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("获取 Linux 主板序列号失败: {}", e.getMessage());
|
||||
return StrUtil.EMPTY;
|
||||
return StringConstants.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +218,7 @@ public class ServerInfoUtils {
|
||||
* @return {@link String}
|
||||
*/
|
||||
private static String getWindowMainBoardSerial() {
|
||||
StringBuilder result = new StringBuilder(StrUtil.EMPTY);
|
||||
StringBuilder result = new StringBuilder(StringConstants.EMPTY);
|
||||
File file = null;
|
||||
BufferedReader input = null;
|
||||
try {
|
||||
@@ -264,10 +263,7 @@ public class ServerInfoUtils {
|
||||
// 获取所有网络接口
|
||||
Set<InetAddress> inetAddresses = getLocalAllInetAddress();
|
||||
if (CollectionUtil.isNotEmpty(inetAddresses)) {
|
||||
return inetAddresses.stream()
|
||||
.map(ServerInfoUtils::getMacByInetAddress)
|
||||
.distinct()
|
||||
.collect(Collectors.toSet());
|
||||
return CollUtils.mapToSet(inetAddresses, ServerInfoUtils::getMacByInetAddress);
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
@@ -282,11 +278,7 @@ public class ServerInfoUtils {
|
||||
// 获取所有网络接口
|
||||
Set<InetAddress> inetAddresses = getLocalAllInetAddress();
|
||||
if (CollectionUtil.isNotEmpty(inetAddresses)) {
|
||||
return inetAddresses.stream()
|
||||
.map(InetAddress::getHostAddress)
|
||||
.distinct()
|
||||
.map(String::toLowerCase)
|
||||
.collect(Collectors.toSet());
|
||||
return CollUtils.mapToSet(inetAddresses, InetAddress::getHostAddress);
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.lang.annotation.Target;
|
||||
* 字段加/解密注解
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@@ -37,7 +38,7 @@ public @interface FieldEncrypt {
|
||||
/**
|
||||
* 加密/解密算法
|
||||
*/
|
||||
Algorithm value() default Algorithm.AES;
|
||||
Algorithm value() default Algorithm.DEFAULT;
|
||||
|
||||
/**
|
||||
* 加密/解密处理器
|
||||
@@ -51,4 +52,14 @@ public @interface FieldEncrypt {
|
||||
* 对称加密算法密钥
|
||||
*/
|
||||
String password() default "";
|
||||
|
||||
/**
|
||||
* 非对称加密算法公钥:RSA需要
|
||||
*/
|
||||
String publicKey() default "";
|
||||
|
||||
/**
|
||||
* 非对称加密算法私钥:RSA需要
|
||||
*/
|
||||
String privateKey() default "";
|
||||
}
|
||||
@@ -27,11 +27,13 @@ import org.springframework.context.annotation.Bean;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.continew.starter.security.crypto.core.MyBatisDecryptInterceptor;
|
||||
import top.continew.starter.security.crypto.core.MyBatisEncryptInterceptor;
|
||||
import top.continew.starter.security.crypto.utils.EncryptHelper;
|
||||
|
||||
/**
|
||||
* 加/解密自动配置
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@@ -51,8 +53,8 @@ public class CryptoAutoConfiguration {
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public MyBatisEncryptInterceptor myBatisEncryptInterceptor() {
|
||||
return new MyBatisEncryptInterceptor(properties);
|
||||
public MyBatisEncryptInterceptor mybatisEncryptInterceptor() {
|
||||
return new MyBatisEncryptInterceptor();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,12 +62,13 @@ public class CryptoAutoConfiguration {
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(MyBatisDecryptInterceptor.class)
|
||||
public MyBatisDecryptInterceptor myBatisDecryptInterceptor() {
|
||||
return new MyBatisDecryptInterceptor(properties);
|
||||
public MyBatisDecryptInterceptor mybatisDecryptInterceptor() {
|
||||
return new MyBatisDecryptInterceptor();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct() {
|
||||
EncryptHelper.init(properties);
|
||||
log.debug("[ContiNew Starter] - Auto Configuration 'Security-Crypto' completed initialization.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,13 @@ package top.continew.starter.security.crypto.autoconfigure;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.continew.starter.security.crypto.enums.Algorithm;
|
||||
|
||||
/**
|
||||
* 加/解密配置属性
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
@ConfigurationProperties(PropertiesConstants.SECURITY_CRYPTO)
|
||||
@@ -33,6 +35,11 @@ public class CryptoProperties {
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 默认算法
|
||||
*/
|
||||
private Algorithm algorithm = Algorithm.AES;
|
||||
|
||||
/**
|
||||
* 对称加密算法密钥
|
||||
*/
|
||||
@@ -56,6 +63,14 @@ public class CryptoProperties {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public Algorithm getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
public void setAlgorithm(Algorithm algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
@@ -18,14 +18,11 @@ package top.continew.starter.security.crypto.core;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.exception.BaseException;
|
||||
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
|
||||
import top.continew.starter.security.crypto.encryptor.IEncryptor;
|
||||
import top.continew.starter.security.crypto.enums.Algorithm;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
@@ -71,23 +68,6 @@ public abstract class AbstractMyBatisInterceptor {
|
||||
.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段加/解密处理器
|
||||
*
|
||||
* @param fieldEncrypt 字段加密注解
|
||||
* @return 加/解密处理器
|
||||
*/
|
||||
protected IEncryptor getEncryptor(FieldEncrypt fieldEncrypt) {
|
||||
Class<? extends IEncryptor> encryptorClass = fieldEncrypt.encryptor();
|
||||
// 使用预定义加/解密处理器
|
||||
if (encryptorClass == IEncryptor.class) {
|
||||
Algorithm algorithm = fieldEncrypt.value();
|
||||
return ReflectUtil.newInstance(algorithm.getEncryptor());
|
||||
}
|
||||
// 使用自定义加/解密处理器
|
||||
return SpringUtil.getBean(encryptorClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密参数
|
||||
*
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package top.continew.starter.security.crypto.core;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import org.apache.ibatis.executor.resultset.ResultSetHandler;
|
||||
@@ -25,39 +26,30 @@ import org.apache.ibatis.plugin.Intercepts;
|
||||
import org.apache.ibatis.plugin.Invocation;
|
||||
import org.apache.ibatis.plugin.Signature;
|
||||
import org.apache.ibatis.type.SimpleTypeRegistry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
|
||||
import top.continew.starter.security.crypto.autoconfigure.CryptoProperties;
|
||||
import top.continew.starter.security.crypto.encryptor.IEncryptor;
|
||||
import top.continew.starter.security.crypto.utils.EncryptHelper;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.sql.Statement;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 字段解密拦截器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
|
||||
public class MyBatisDecryptInterceptor extends AbstractMyBatisInterceptor implements Interceptor {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MyBatisDecryptInterceptor.class);
|
||||
private CryptoProperties properties;
|
||||
|
||||
public MyBatisDecryptInterceptor(CryptoProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public MyBatisDecryptInterceptor() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object intercept(Invocation invocation) throws Throwable {
|
||||
// 获取执行结果
|
||||
Object obj = invocation.proceed();
|
||||
if (obj == null) {
|
||||
if (ObjectUtil.isNull(obj)) {
|
||||
return null;
|
||||
}
|
||||
// 确保目标是 ResultSetHandler
|
||||
@@ -68,6 +60,9 @@ public class MyBatisDecryptInterceptor extends AbstractMyBatisInterceptor implem
|
||||
if (obj instanceof List<?> resultList) {
|
||||
// 处理列表结果
|
||||
this.decryptList(resultList);
|
||||
} else if (obj instanceof Map<?, ?> map) {
|
||||
// 处理Map结果
|
||||
this.decryptMap(map);
|
||||
} else {
|
||||
// 处理单个对象结果
|
||||
this.decryptObject(obj);
|
||||
@@ -89,13 +84,25 @@ public class MyBatisDecryptInterceptor extends AbstractMyBatisInterceptor implem
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密Map结果
|
||||
*
|
||||
* @param resultMap 结果Map
|
||||
*/
|
||||
private void decryptMap(Map<?, ?> resultMap) {
|
||||
if (CollUtil.isEmpty(resultMap)) {
|
||||
return;
|
||||
}
|
||||
new HashSet<>(resultMap.values()).forEach(this::decryptObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密单个对象结果
|
||||
*
|
||||
* @param result 结果对象
|
||||
*/
|
||||
private void decryptObject(Object result) {
|
||||
if (result == null) {
|
||||
if (ObjectUtil.isNull(result)) {
|
||||
return;
|
||||
}
|
||||
// String、Integer、Long 等简单类型对象无需处理
|
||||
@@ -109,21 +116,16 @@ public class MyBatisDecryptInterceptor extends AbstractMyBatisInterceptor implem
|
||||
}
|
||||
// 解密处理
|
||||
for (Field field : fieldList) {
|
||||
IEncryptor encryptor = super.getEncryptor(field.getAnnotation(FieldEncrypt.class));
|
||||
Object fieldValue = ReflectUtil.getFieldValue(result, field);
|
||||
if (fieldValue == null) {
|
||||
continue;
|
||||
}
|
||||
// 优先获取自定义对称加密算法密钥,获取不到时再获取全局配置
|
||||
String password = ObjectUtil.defaultIfBlank(field.getAnnotation(FieldEncrypt.class).password(), properties
|
||||
.getPassword());
|
||||
try {
|
||||
String ciphertext = encryptor.decrypt(fieldValue.toString(), password, properties.getPrivateKey());
|
||||
ReflectUtil.setFieldValue(result, field, ciphertext);
|
||||
} catch (Exception e) {
|
||||
// 解密失败时保留原值,避免影响正常业务流程
|
||||
log.warn("解密失败,请检查加密配置", e);
|
||||
String strValue = String.valueOf(fieldValue);
|
||||
if (CharSequenceUtil.isBlank(strValue)) {
|
||||
continue;
|
||||
}
|
||||
ReflectUtil.setFieldValue(result, field, EncryptHelper.decrypt(strValue, field
|
||||
.getAnnotation(FieldEncrypt.class)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,12 +28,9 @@ import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.session.ResultHandler;
|
||||
import org.apache.ibatis.session.RowBounds;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
|
||||
import top.continew.starter.security.crypto.autoconfigure.CryptoProperties;
|
||||
import top.continew.starter.security.crypto.encryptor.IEncryptor;
|
||||
import top.continew.starter.security.crypto.utils.EncryptHelper;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
@@ -47,18 +44,13 @@ import java.util.regex.Pattern;
|
||||
* 字段加密拦截器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor implements InnerInterceptor {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MyBatisEncryptInterceptor.class);
|
||||
private static final Pattern PARAM_PAIRS_PATTERN = Pattern
|
||||
.compile("#\\{ew\\.paramNameValuePairs\\.(" + Constants.WRAPPER_PARAM + "\\d+)\\}");
|
||||
private final CryptoProperties properties;
|
||||
|
||||
public MyBatisEncryptInterceptor(CryptoProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeQuery(Executor executor,
|
||||
@@ -124,22 +116,16 @@ public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor implem
|
||||
*/
|
||||
private void encryptEntity(List<Field> fieldList, Object entity) {
|
||||
for (Field field : fieldList) {
|
||||
IEncryptor encryptor = super.getEncryptor(field.getAnnotation(FieldEncrypt.class));
|
||||
Object fieldValue = ReflectUtil.getFieldValue(entity, field);
|
||||
if (fieldValue == null) {
|
||||
continue;
|
||||
}
|
||||
// 优先获取自定义对称加密算法密钥,获取不到时再获取全局配置
|
||||
String password = ObjectUtil.defaultIfBlank(field.getAnnotation(FieldEncrypt.class).password(), properties
|
||||
.getPassword());
|
||||
String ciphertext = fieldValue.toString();
|
||||
try {
|
||||
ciphertext = encryptor.encrypt(fieldValue.toString(), password, properties.getPublicKey());
|
||||
} catch (Exception e) {
|
||||
// 加密失败时保留原值,避免影响正常业务流程
|
||||
log.warn("加密失败,请检查加密配置", e);
|
||||
String strValue = String.valueOf(fieldValue);
|
||||
if (CharSequenceUtil.isBlank(strValue)) {
|
||||
continue;
|
||||
}
|
||||
ReflectUtil.setFieldValue(entity, field, ciphertext);
|
||||
ReflectUtil.setFieldValue(entity, field, EncryptHelper.encrypt(strValue, field
|
||||
.getAnnotation(FieldEncrypt.class)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,8 +183,7 @@ public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor implem
|
||||
if (matcher.matches()) {
|
||||
String valueKey = matcher.group(1);
|
||||
Object value = updateWrapper.getParamNameValuePairs().get(valueKey);
|
||||
Object ciphertext = this.doEncrypt(value, fieldEncrypt);
|
||||
updateWrapper.getParamNameValuePairs().put(valueKey, ciphertext);
|
||||
updateWrapper.getParamNameValuePairs().put(valueKey, this.doEncrypt(value, fieldEncrypt));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,19 +196,14 @@ public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor implem
|
||||
* @param fieldEncrypt 字段加密注解
|
||||
*/
|
||||
private Object doEncrypt(Object parameterValue, FieldEncrypt fieldEncrypt) {
|
||||
if (parameterValue == null) {
|
||||
if (ObjectUtil.isNull(parameterValue)) {
|
||||
return null;
|
||||
}
|
||||
IEncryptor encryptor = super.getEncryptor(fieldEncrypt);
|
||||
// 优先获取自定义对称加密算法密钥,获取不到时再获取全局配置
|
||||
String password = ObjectUtil.defaultIfBlank(fieldEncrypt.password(), properties.getPassword());
|
||||
try {
|
||||
return encryptor.encrypt(parameterValue.toString(), password, properties.getPublicKey());
|
||||
} catch (Exception e) {
|
||||
// 加密失败时保留原值,避免影响正常业务流程
|
||||
log.warn("加密失败,请检查加密配置", e);
|
||||
String strValue = String.valueOf(parameterValue);
|
||||
if (CharSequenceUtil.isBlank(strValue)) {
|
||||
return null;
|
||||
}
|
||||
return parameterValue;
|
||||
return EncryptHelper.encrypt(strValue, fieldEncrypt);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,21 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.extension.tenant;
|
||||
package top.continew.starter.security.crypto.encryptor;
|
||||
|
||||
/**
|
||||
* 租户处理器
|
||||
* 加密器基类
|
||||
*
|
||||
* @author 小熊
|
||||
* @since 2.8.0
|
||||
* @author lishuyan
|
||||
* @since 2.13.2
|
||||
*/
|
||||
public interface TenantHandler {
|
||||
public abstract class AbstractEncryptor implements IEncryptor {
|
||||
|
||||
protected AbstractEncryptor(CryptoContext context) {
|
||||
// 配置校验与配置注入
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定租户中执行
|
||||
*
|
||||
* @param tenantId 租户 ID
|
||||
* @param runnable 方法
|
||||
*/
|
||||
void execute(Long tenantId, Runnable runnable);
|
||||
}
|
||||
@@ -29,26 +29,40 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
* 对称加/解密处理器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public abstract class AbstractSymmetricCryptoEncryptor implements IEncryptor {
|
||||
public abstract class AbstractSymmetricCryptoEncryptor extends AbstractEncryptor {
|
||||
|
||||
/**
|
||||
* 对称加密缓存
|
||||
*/
|
||||
private static final Map<String, SymmetricCrypto> CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public String encrypt(String plaintext, String password, String publicKey) throws Exception {
|
||||
if (CharSequenceUtil.isBlank(plaintext)) {
|
||||
return plaintext;
|
||||
}
|
||||
return this.getCrypto(password).encryptHex(plaintext);
|
||||
/**
|
||||
* 加密上下文
|
||||
*/
|
||||
private final CryptoContext context;
|
||||
|
||||
protected AbstractSymmetricCryptoEncryptor(CryptoContext context) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String ciphertext, String password, String privateKey) throws Exception {
|
||||
public String encrypt(String plaintext) {
|
||||
if (CharSequenceUtil.isBlank(plaintext)) {
|
||||
return plaintext;
|
||||
}
|
||||
return this.getCrypto(context.getPassword()).encryptHex(plaintext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String ciphertext) {
|
||||
if (CharSequenceUtil.isBlank(ciphertext)) {
|
||||
return ciphertext;
|
||||
}
|
||||
return this.getCrypto(password).decryptStr(ciphertext);
|
||||
return this.getCrypto(context.getPassword()).decryptStr(ciphertext);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,10 @@ import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
|
||||
*/
|
||||
public class AesEncryptor extends AbstractSymmetricCryptoEncryptor {
|
||||
|
||||
public AesEncryptor(CryptoContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SymmetricAlgorithm getAlgorithm() {
|
||||
return SymmetricAlgorithm.AES;
|
||||
|
||||
@@ -30,12 +30,12 @@ import cn.hutool.core.codec.Base64;
|
||||
public class Base64Encryptor implements IEncryptor {
|
||||
|
||||
@Override
|
||||
public String encrypt(String plaintext, String password, String publicKey) throws Exception {
|
||||
public String encrypt(String plaintext) {
|
||||
return Base64.encode(plaintext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String ciphertext, String password, String privateKey) throws Exception {
|
||||
public String decrypt(String ciphertext) {
|
||||
return Base64.decodeStr(ciphertext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.security.crypto.encryptor;
|
||||
|
||||
import top.continew.starter.security.crypto.enums.Algorithm;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 加密上下文
|
||||
*
|
||||
* @author lishuyan
|
||||
* @since 2.13.2
|
||||
*/
|
||||
public class CryptoContext {
|
||||
|
||||
/**
|
||||
* 默认算法
|
||||
*/
|
||||
private Algorithm algorithm;
|
||||
|
||||
/**
|
||||
* 对称加密算法密钥
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 非对称加密算法公钥
|
||||
*/
|
||||
private String publicKey;
|
||||
|
||||
/**
|
||||
* 非对称加密算法私钥
|
||||
*/
|
||||
private String privateKey;
|
||||
|
||||
public Algorithm getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
public void setAlgorithm(Algorithm algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public void setPublicKey(String publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
public String getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
public void setPrivateKey(String privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
CryptoContext that = (CryptoContext)o;
|
||||
return algorithm == that.algorithm && Objects.equals(password, that.password) && Objects
|
||||
.equals(publicKey, that.publicKey) && Objects.equals(privateKey, that.privateKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(algorithm, password, publicKey, privateKey);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,10 @@ import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
|
||||
*/
|
||||
public class DesEncryptor extends AbstractSymmetricCryptoEncryptor {
|
||||
|
||||
public DesEncryptor(CryptoContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SymmetricAlgorithm getAlgorithm() {
|
||||
return SymmetricAlgorithm.DES;
|
||||
|
||||
@@ -20,6 +20,7 @@ package top.continew.starter.security.crypto.encryptor;
|
||||
* 加/解密接口
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public interface IEncryptor {
|
||||
@@ -28,21 +29,15 @@ public interface IEncryptor {
|
||||
* 加密
|
||||
*
|
||||
* @param plaintext 明文
|
||||
* @param password 对称加密算法密钥
|
||||
* @param publicKey 非对称加密算法公钥
|
||||
* @return 加密后的文本
|
||||
* @throws Exception /
|
||||
*/
|
||||
String encrypt(String plaintext, String password, String publicKey) throws Exception;
|
||||
String encrypt(String plaintext);
|
||||
|
||||
/**
|
||||
* 解密
|
||||
*
|
||||
* @param ciphertext 密文
|
||||
* @param password 对称加密算法密钥
|
||||
* @param privateKey 非对称加密算法私钥
|
||||
* @return 解密后的文本
|
||||
* @throws Exception /
|
||||
*/
|
||||
String decrypt(String ciphertext, String password, String privateKey) throws Exception;
|
||||
String decrypt(String ciphertext);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
|
||||
*/
|
||||
public class PbeWithMd5AndDesEncryptor extends AbstractSymmetricCryptoEncryptor {
|
||||
|
||||
public PbeWithMd5AndDesEncryptor(CryptoContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SymmetricAlgorithm getAlgorithm() {
|
||||
return SymmetricAlgorithm.PBEWithMD5AndDES;
|
||||
|
||||
@@ -27,17 +27,29 @@ import cn.hutool.crypto.asymmetric.KeyType;
|
||||
* </p>
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public class RsaEncryptor implements IEncryptor {
|
||||
public class RsaEncryptor extends AbstractEncryptor {
|
||||
|
||||
@Override
|
||||
public String encrypt(String plaintext, String password, String publicKey) throws Exception {
|
||||
return Base64.encode(SecureUtil.rsa(null, publicKey).encrypt(plaintext, KeyType.PublicKey));
|
||||
/**
|
||||
* 加密上下文
|
||||
*/
|
||||
private final CryptoContext context;
|
||||
|
||||
public RsaEncryptor(CryptoContext context) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String ciphertext, String password, String privateKey) throws Exception {
|
||||
return new String(SecureUtil.rsa(privateKey, null).decrypt(Base64.decode(ciphertext), KeyType.PrivateKey));
|
||||
public String encrypt(String plaintext) {
|
||||
return Base64.encode(SecureUtil.rsa(null, context.getPublicKey()).encrypt(plaintext, KeyType.PublicKey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String ciphertext) {
|
||||
return new String(SecureUtil.rsa(context.getPrivateKey(), null)
|
||||
.decrypt(Base64.decode(ciphertext), KeyType.PrivateKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,16 @@ import top.continew.starter.security.crypto.encryptor.*;
|
||||
* 加密/解密算法枚举
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public enum Algorithm {
|
||||
|
||||
/**
|
||||
* 默认使用配置属性的算法
|
||||
*/
|
||||
DEFAULT(null),
|
||||
|
||||
/**
|
||||
* AES
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* 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.security.crypto.utils;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
|
||||
import top.continew.starter.security.crypto.autoconfigure.CryptoProperties;
|
||||
import top.continew.starter.security.crypto.encryptor.CryptoContext;
|
||||
import top.continew.starter.security.crypto.encryptor.IEncryptor;
|
||||
import top.continew.starter.security.crypto.enums.Algorithm;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 加密助手
|
||||
*
|
||||
* @author lishuyan
|
||||
* @since 2.13.2
|
||||
*/
|
||||
public class EncryptHelper {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EncryptHelper.class);
|
||||
|
||||
/**
|
||||
* 默认加密配置
|
||||
*/
|
||||
private static CryptoProperties defaultProperties;
|
||||
|
||||
/**
|
||||
* 加密器缓存
|
||||
*/
|
||||
private static final Map<Integer, IEncryptor> ENCRYPTOR_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private EncryptHelper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认配置
|
||||
*
|
||||
* @param properties 加密配置
|
||||
*/
|
||||
public static void init(CryptoProperties properties) {
|
||||
defaultProperties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册加密执行者到缓存
|
||||
* <p>
|
||||
* 计算 CryptoContext 对象的hashCode作为缓存中的key,通过hashCode查询缓存中存在则直接返回,不存在则创建并缓存
|
||||
* </p>
|
||||
*
|
||||
* @param encryptContext 加密执行者需要的相关配置参数
|
||||
* @return 加密执行者
|
||||
*/
|
||||
public static IEncryptor registerAndGetEncryptor(CryptoContext encryptContext) {
|
||||
int key = encryptContext.hashCode();
|
||||
return ENCRYPTOR_CACHE.computeIfAbsent(key, k -> ReflectUtil.newInstance(encryptContext.getAlgorithm()
|
||||
.getEncryptor(), encryptContext));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段上的 @FieldEncrypt 注解
|
||||
*
|
||||
* @param obj 对象
|
||||
* @param fieldName 字段名称
|
||||
* @return 字段上的 @FieldEncrypt 注解
|
||||
* @throws NoSuchFieldException /
|
||||
*/
|
||||
public static FieldEncrypt getFieldEncrypt(Object obj, String fieldName) throws NoSuchFieldException {
|
||||
Field field = obj.getClass().getDeclaredField(fieldName);
|
||||
return field.getAnnotation(FieldEncrypt.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密方法
|
||||
*
|
||||
* @param value 待加密字符串
|
||||
* @param fieldEncrypt 待加密字段注解
|
||||
* @return 加密后的字符串
|
||||
*/
|
||||
public static String encrypt(String value, FieldEncrypt fieldEncrypt) {
|
||||
if (CharSequenceUtil.isBlank(value) || fieldEncrypt == null || !defaultProperties.isEnabled()) {
|
||||
return value;
|
||||
}
|
||||
String ciphertext = value;
|
||||
try {
|
||||
CryptoContext encryptContext = buildEncryptContext(fieldEncrypt);
|
||||
IEncryptor encryptor = registerAndGetEncryptor(encryptContext);
|
||||
ciphertext = encryptor.encrypt(ciphertext);
|
||||
} catch (Exception e) {
|
||||
log.warn("加密失败,请检查加密配置,处理加密字段异常:{}", e.getMessage(), e);
|
||||
}
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密方法
|
||||
*
|
||||
* @param value 待加密字符串
|
||||
* @return 加密后的字符串
|
||||
*/
|
||||
public static String encrypt(String value) {
|
||||
if (CharSequenceUtil.isBlank(value) || !defaultProperties.isEnabled()) {
|
||||
return value;
|
||||
}
|
||||
String ciphertext = value;
|
||||
try {
|
||||
CryptoContext encryptContext = buildEncryptContext();
|
||||
IEncryptor encryptor = registerAndGetEncryptor(encryptContext);
|
||||
ciphertext = encryptor.encrypt(ciphertext);
|
||||
} catch (Exception e) {
|
||||
log.warn("加密失败,请检查加密配置,处理加密字段异常:{}", e.getMessage(), e);
|
||||
}
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密方法
|
||||
*
|
||||
* @param value 待解密字符串
|
||||
* @param fieldEncrypt 待解密字段注解
|
||||
* @return 解密后的字符串
|
||||
*/
|
||||
public static String decrypt(String value, FieldEncrypt fieldEncrypt) {
|
||||
if (CharSequenceUtil.isBlank(value) || fieldEncrypt == null || !defaultProperties.isEnabled()) {
|
||||
return value;
|
||||
}
|
||||
String plaintext = value;
|
||||
try {
|
||||
CryptoContext encryptContext = buildEncryptContext(fieldEncrypt);
|
||||
IEncryptor encryptor = registerAndGetEncryptor(encryptContext);
|
||||
plaintext = encryptor.decrypt(plaintext);
|
||||
} catch (Exception e) {
|
||||
log.warn("解密失败,请检查加密配置,处理解密字段异常:{}", e.getMessage(), e);
|
||||
}
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密方法
|
||||
*
|
||||
* @param value 待解密字符串
|
||||
* @return 解密后的字符串
|
||||
*/
|
||||
public static String decrypt(String value) {
|
||||
if (CharSequenceUtil.isBlank(value) || !defaultProperties.isEnabled()) {
|
||||
return value;
|
||||
}
|
||||
String plaintext = value;
|
||||
try {
|
||||
CryptoContext encryptContext = buildEncryptContext();
|
||||
IEncryptor encryptor = registerAndGetEncryptor(encryptContext);
|
||||
plaintext = encryptor.decrypt(plaintext);
|
||||
} catch (Exception e) {
|
||||
log.warn("解密失败,请检查加密配置,处理解密字段异常:{}", e.getMessage(), e);
|
||||
}
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建加密上下文
|
||||
*
|
||||
* @param fieldEncrypt 字段加密注解
|
||||
* @return 加密上下文
|
||||
*/
|
||||
private static CryptoContext buildEncryptContext(FieldEncrypt fieldEncrypt) {
|
||||
CryptoContext encryptContext = new CryptoContext();
|
||||
encryptContext.setAlgorithm(fieldEncrypt.value() == Algorithm.DEFAULT
|
||||
? defaultProperties.getAlgorithm()
|
||||
: fieldEncrypt.value());
|
||||
encryptContext.setPassword(fieldEncrypt.password().isEmpty()
|
||||
? defaultProperties.getPassword()
|
||||
: fieldEncrypt.password());
|
||||
encryptContext.setPrivateKey(fieldEncrypt.privateKey().isEmpty()
|
||||
? defaultProperties.getPrivateKey()
|
||||
: fieldEncrypt.privateKey());
|
||||
encryptContext.setPublicKey(fieldEncrypt.publicKey().isEmpty()
|
||||
? defaultProperties.getPublicKey()
|
||||
: fieldEncrypt.publicKey());
|
||||
return encryptContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建加密上下文
|
||||
*
|
||||
* @return 加密上下文
|
||||
*/
|
||||
private static CryptoContext buildEncryptContext() {
|
||||
CryptoContext encryptContext = new CryptoContext();
|
||||
encryptContext.setAlgorithm(defaultProperties.getAlgorithm());
|
||||
encryptContext.setPassword(defaultProperties.getPassword());
|
||||
encryptContext.setPrivateKey(defaultProperties.getPrivateKey());
|
||||
encryptContext.setPublicKey(defaultProperties.getPublicKey());
|
||||
return encryptContext;
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ public @interface JsonMask {
|
||||
/**
|
||||
* 左侧保留位数
|
||||
* <p>
|
||||
* 仅在脱敏类型为 {@code DesensitizedType.CUSTOM } 时使用
|
||||
* 仅在脱敏类型为 {@code MaskType.CUSTOM } 时使用
|
||||
* </p>
|
||||
*/
|
||||
int left() default 0;
|
||||
@@ -64,7 +64,7 @@ public @interface JsonMask {
|
||||
/**
|
||||
* 右侧保留位数
|
||||
* <p>
|
||||
* 仅在脱敏类型为 {@code DesensitizedType.CUSTOM } 时使用
|
||||
* 仅在脱敏类型为 {@code MaskType.CUSTOM } 时使用
|
||||
* </p>
|
||||
*/
|
||||
int right() default 0;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package top.continew.starter.storage.enums;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import top.continew.starter.core.enums.BaseEnum;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -69,7 +69,7 @@ public enum FileType implements BaseEnum<Integer> {
|
||||
*/
|
||||
public static FileType getByExtension(String extension) {
|
||||
return Arrays.stream(FileType.values())
|
||||
.filter(t -> t.getExtensions().contains(StrUtil.emptyIfNull(extension).toLowerCase()))
|
||||
.filter(t -> t.getExtensions().contains(CharSequenceUtil.emptyIfNull(extension).toLowerCase()))
|
||||
.findFirst()
|
||||
.orElse(FileType.UNKNOWN);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ package top.continew.starter.storage.util;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -121,7 +121,7 @@ public class StorageUtils {
|
||||
// 获取文件的扩展名
|
||||
String extName = FileNameUtil.extName(fileName);
|
||||
// 去掉扩展名
|
||||
String baseName = StrUtil.subBefore(fileName, StringConstants.DOT, true);
|
||||
String baseName = CharSequenceUtil.subBefore(fileName, StringConstants.DOT, true);
|
||||
// 拼接新的路径:原始路径 + .缩略图后缀 + .扩展名
|
||||
return baseName + "." + suffix + "." + extName;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ package top.continew.starter.storage.strategy;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.exception.BusinessException;
|
||||
@@ -123,7 +123,7 @@ public class LocalStorageStrategy implements StorageStrategy<LocalClient> {
|
||||
// 格式化文件名 防止上传后重复
|
||||
String formatFileName = StorageUtils.formatFileName(fileName);
|
||||
// 判断文件路径是否为空 为空给默认路径 格式 2024/12/30/
|
||||
if (StrUtil.isEmpty(path)) {
|
||||
if (CharSequenceUtil.isEmpty(path)) {
|
||||
path = StorageUtils.localDefaultPath();
|
||||
}
|
||||
// 判断文件夹是否存在 不存在则创建
|
||||
|
||||
@@ -19,8 +19,8 @@ package top.continew.starter.storage.strategy;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -156,7 +156,7 @@ public class OssStorageStrategy implements StorageStrategy<OssClient> {
|
||||
// 格式化文件名 防止上传后重复
|
||||
String formatFileName = StorageUtils.formatFileName(fileName);
|
||||
// 判断文件路径是否为空 为空给默认路径 格式 2024/12/30/
|
||||
if (StrUtil.isEmpty(path)) {
|
||||
if (CharSequenceUtil.isEmpty(path)) {
|
||||
path = StorageUtils.ossDefaultPath();
|
||||
}
|
||||
ThumbnailResp thumbnailResp = null;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package top.continew.starter.storage.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.storage.constant.StorageConstant;
|
||||
@@ -39,7 +39,7 @@ public class OssUtils {
|
||||
* @return {@link Region }
|
||||
*/
|
||||
public static Region getRegion(String region) {
|
||||
return StrUtil.isEmpty(region) ? Region.US_EAST_1 : Region.of(region);
|
||||
return CharSequenceUtil.isEmpty(region) ? Region.US_EAST_1 : Region.of(region);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,7 +51,7 @@ public class OssUtils {
|
||||
*/
|
||||
public static String getUrl(String endpoint, String bucketName) {
|
||||
// 如果是云服务商,直接返回域名或终端点
|
||||
if (StrUtil.containsAny(endpoint, StorageConstant.CLOUD_SERVICE_PREFIX)) {
|
||||
if (CharSequenceUtil.containsAny(endpoint, StorageConstant.CLOUD_SERVICE_PREFIX)) {
|
||||
return "http://" + bucketName + StringConstants.DOT + endpoint;
|
||||
} else {
|
||||
return "http://" + endpoint + StringConstants.SLASH + bucketName;
|
||||
|
||||
@@ -23,9 +23,10 @@ package top.continew.starter.trace.autoconfigure;
|
||||
* 重写 TLog 配置以适配 Spring Boot 3.x
|
||||
* </p>
|
||||
*
|
||||
* @see com.yomahub.tlog.springboot.property.TLogProperty
|
||||
*
|
||||
* @author Bryan.Zhang
|
||||
* @author Jasmine
|
||||
* @see com.yomahub.tlog.springboot.property.TLogProperty
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public class TLogProperties {
|
||||
|
||||
@@ -33,9 +33,10 @@ import java.io.IOException;
|
||||
* 重写 TLog 配置以适配 Spring Boot 3.x
|
||||
* </p>
|
||||
*
|
||||
* @see com.yomahub.tlog.web.filter.TLogServletFilter
|
||||
*
|
||||
* @author Bryan.Zhang
|
||||
* @author Jasmine
|
||||
* @see com.yomahub.tlog.web.filter.TLogServletFilter
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public class TLogServletFilter implements Filter {
|
||||
|
||||
@@ -28,9 +28,10 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
* 重写 TLog 配置以适配 Spring Boot 3.x
|
||||
* </p>
|
||||
*
|
||||
* @see com.yomahub.tlog.web.common.TLogWebCommon
|
||||
*
|
||||
* @author Bryan.Zhang
|
||||
* @author Jasmine
|
||||
* @see com.yomahub.tlog.web.common.TLogWebCommon
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public class TLogWebCommon extends TLogRPCHandler {
|
||||
|
||||
@@ -18,26 +18,24 @@ package top.continew.starter.validation.autoconfigure;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.validation.Validator;
|
||||
import org.hibernate.validator.HibernateValidator;
|
||||
import org.hibernate.validator.BaseHibernateValidatorConfiguration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* JSR 303 校验器自动配置
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
public class ValidatorAutoConfiguration {
|
||||
@AutoConfigureBefore
|
||||
public class ValidationAutoConfiguration {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ValidatorAutoConfiguration.class);
|
||||
private static final Logger log = LoggerFactory.getLogger(ValidationAutoConfiguration.class);
|
||||
|
||||
/**
|
||||
* Validator 失败立即返回模式配置
|
||||
@@ -51,10 +49,9 @@ public class ValidatorAutoConfiguration {
|
||||
try (LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean()) {
|
||||
// 国际化
|
||||
factoryBean.setValidationMessageSource(messageSource);
|
||||
factoryBean.setProviderClass(HibernateValidator.class);
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("hibernate.validator.fail_fast", "true");
|
||||
factoryBean.setValidationProperties(properties);
|
||||
// 快速失败
|
||||
factoryBean.getValidationPropertyMap()
|
||||
.put(BaseHibernateValidatorConfiguration.FAIL_FAST, Boolean.TRUE.toString());
|
||||
factoryBean.afterPropertiesSet();
|
||||
return factoryBean.getValidator();
|
||||
}
|
||||
@@ -62,6 +59,6 @@ public class ValidatorAutoConfiguration {
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct() {
|
||||
log.debug("[ContiNew Starter] - Auto Configuration 'Validator' completed initialization.");
|
||||
log.debug("[ContiNew Starter] - Auto Configuration 'Validation' completed initialization.");
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,10 @@ import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import top.continew.starter.core.enums.BaseEnum;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 枚举校验器
|
||||
@@ -53,17 +53,53 @@ public class EnumValueValidator implements ConstraintValidator<EnumValue, Object
|
||||
if (value == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理数组场景
|
||||
if (value.getClass().isArray()) {
|
||||
Object[] array = (Object[])value;
|
||||
for (Object element : array) {
|
||||
if (!isValidElement(element)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理集合场景
|
||||
if (value instanceof Iterable<?> iterable) {
|
||||
for (Object element : iterable) {
|
||||
if (!isValidElement(element)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理单个值场景
|
||||
return isValidElement(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验单个元素是否有效
|
||||
*
|
||||
* @param value 待校验的值
|
||||
* @return 是否有效
|
||||
*/
|
||||
private boolean isValidElement(Object value) {
|
||||
// 优先校验 enumValues
|
||||
if (enumValues.length > 0) {
|
||||
return Arrays.asList(enumValues).contains(Convert.toStr(value));
|
||||
}
|
||||
|
||||
Enum[] enumConstants = enumClass.getEnumConstants();
|
||||
if (enumConstants.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CharSequenceUtil.isBlank(enumMethod)) {
|
||||
return findEnumValue(enumConstants, Enum::toString, Convert.toStr(value));
|
||||
return findEnumValue(enumConstants, Convert.toStr(value));
|
||||
}
|
||||
|
||||
try {
|
||||
// 枚举类指定了方法名,则调用指定方法获取枚举值
|
||||
Method method = enumClass.getMethod(enumMethod);
|
||||
@@ -82,16 +118,19 @@ public class EnumValueValidator implements ConstraintValidator<EnumValue, Object
|
||||
* 遍历枚举类,判断是否包含指定值
|
||||
*
|
||||
* @param enumConstants 枚举类数组
|
||||
* @param function 获取枚举值的函数
|
||||
* @param value 待校验的值
|
||||
* @return 是否包含指定值
|
||||
*/
|
||||
private boolean findEnumValue(Enum[] enumConstants, Function<Enum, Object> function, Object value) {
|
||||
private boolean findEnumValue(Enum[] enumConstants, Object value) {
|
||||
for (Enum enumConstant : enumConstants) {
|
||||
if (function.apply(enumConstant).equals(value)) {
|
||||
if (enumConstant instanceof BaseEnum<?> baseEnum) {
|
||||
if (baseEnum.getValue().toString().equals(value)) {
|
||||
return true;
|
||||
}
|
||||
} else if (enumConstant.toString().equals(value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package top.continew.starter.validation.constraints;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
@@ -30,7 +31,7 @@ public class JsonStringValidator implements ConstraintValidator<JsonString, Stri
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (value == null) {
|
||||
if (CharSequenceUtil.isBlank(value)) {
|
||||
return true;
|
||||
}
|
||||
return JSONUtil.isTypeJSON(value);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package top.continew.starter.validation.constraints;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.PhoneUtil;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
@@ -34,7 +35,7 @@ public class MobileValidator implements ConstraintValidator<Mobile, String> {
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (value == null) {
|
||||
if (CharSequenceUtil.isBlank(value)) {
|
||||
return true;
|
||||
}
|
||||
return PhoneUtil.isMobile(value);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package top.continew.starter.validation.constraints;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.PhoneUtil;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
@@ -34,7 +35,7 @@ public class PhoneValidator implements ConstraintValidator<Phone, String> {
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (value == null) {
|
||||
if (CharSequenceUtil.isBlank(value)) {
|
||||
return true;
|
||||
}
|
||||
return PhoneUtil.isPhone(value);
|
||||
|
||||
@@ -1 +1 @@
|
||||
top.continew.starter.validation.autoconfigure.ValidatorAutoConfiguration
|
||||
top.continew.starter.validation.autoconfigure.ValidationAutoConfiguration
|
||||
@@ -31,8 +31,7 @@ import io.undertow.Undertow;
|
||||
import io.undertow.server.handlers.DisallowedMethodsHandler;
|
||||
import io.undertow.util.HttpString;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
import top.continew.starter.core.util.CollUtils;
|
||||
|
||||
/**
|
||||
* Undertow 自动配置
|
||||
@@ -57,11 +56,8 @@ public class UndertowAutoConfiguration {
|
||||
public WebServerFactoryCustomizer<UndertowServletWebServerFactory> customize(ServerExtensionProperties properties) {
|
||||
return factory -> {
|
||||
factory.addDeploymentInfoCustomizers(deploymentInfo -> deploymentInfo
|
||||
.addInitialHandlerChainWrapper(handler -> new DisallowedMethodsHandler(handler, properties
|
||||
.getDisallowedMethods()
|
||||
.stream()
|
||||
.map(HttpString::tryFromString)
|
||||
.collect(Collectors.toSet()))));
|
||||
.addInitialHandlerChainWrapper(handler -> new DisallowedMethodsHandler(handler, CollUtils
|
||||
.mapToSet(properties.getDisallowedMethods(), HttpString::tryFromString))));
|
||||
log.debug("[ContiNew Starter] - Disallowed HTTP methods on Server Undertow: {}.", properties
|
||||
.getDisallowedMethods());
|
||||
log.debug("[ContiNew Starter] - Auto Configuration 'Web-Server Undertow' completed initialization.");
|
||||
|
||||
Reference in New Issue
Block a user