mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-11-12 18:57:14 +08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3f57b8a5e | |||
| 2388d32363 | |||
| 9fdbfdf8bb | |||
| e9b81f9466 | |||
| 427143db69 | |||
| 2208dbd028 | |||
| 9e5f33b2c9 | |||
| 260f484af9 | |||
|
|
4caf0a0db6 | ||
| 16b6e9b466 | |||
| eac5c1fa79 | |||
|
|
52fc9d086d | ||
| 5e0eea2ed8 | |||
| 784a56fd4f | |||
| d3fa00d14c | |||
| e55eb17d64 | |||
| 7571c05f9c | |||
| 4c4f98a86e | |||
|
|
bf51837991 | ||
| 22ebdfeb9f | |||
| b0a2a8c927 | |||
| ea6b316296 | |||
| 4786af5104 | |||
| 6693cd49b9 |
53
CHANGELOG.md
53
CHANGELOG.md
@@ -1,3 +1,56 @@
|
||||
## [v2.6.0](https://github.com/continew-org/continew-starter/compare/v2.5.2...v2.6.0) (2024-09-06)
|
||||
|
||||
### ✨ 新特性
|
||||
|
||||
- 【web】新增 isMatch 路径是否匹配方法 ([e55eb17](https://github.com/continew-org/continew-starter/commit/e55eb17d64d7b42c6e64b18a645cda9558f08d58))
|
||||
- 【log】不记录日志也支持开启打印访问日志 ([16b6e9b](https://github.com/continew-org/continew-starter/commit/16b6e9b466578d935ab9a8a85a5eac4d09025dee))
|
||||
|
||||
### 💎 功能优化
|
||||
|
||||
- 【data】移除 DataPermission 注解的 value 属性 ([d3fa00d](https://github.com/continew-org/continew-starter/commit/d3fa00d14ce95ab1bedaebbce8304e29df5da8fd))
|
||||
- 【data】mybatis-plus => mp,mybatis-flex => mf ([5e0eea2](https://github.com/continew-org/continew-starter/commit/5e0eea2ed801b526da9f65b8cb2942b3b7b050ef))
|
||||
- 【web】提升接口文档响应类型优化扩展性 ([784a56f](https://github.com/continew-org/continew-starter/commit/784a56fd4f85ae170b8a56dd0a64a33a7bff98bc))
|
||||
- 【web】链路追踪配置属性响应头名称 => 链路 ID 名称 ([260f484](https://github.com/continew-org/continew-starter/commit/260f484af98d112927edec4316c0375e628dd3ba))
|
||||
- 【log】优化接口耗时相关时间类型使用 ([4caf0a0](https://github.com/continew-org/continew-starter/commit/4caf0a0db69779a5ea409ec7e01c4044817afc94))
|
||||
|
||||
### 🐛 问题修复
|
||||
|
||||
- 【log】修复日志全局 includes 配置会被局部修改的问题 ([eac5c1f](https://github.com/continew-org/continew-starter/commit/eac5c1fa79f7f91217b0525a07e0f1314f8efe24))
|
||||
- 【data】还原 SQL 函数接口 ([9e5f33b](https://github.com/continew-org/continew-starter/commit/9e5f33b2c9b804741f9b77eab5b67ab3c4a8b220))
|
||||
- 【crypto】修复由于升级 mybatis plus 引发的更新场景加密失效问题 ([e9b81f9](https://github.com/continew-org/continew-starter/commit/e9b81f946603e2f9e44b50471102b2551b9d50ac)) ([9fdbfdf](https://github.com/continew-org/continew-starter/commit/9fdbfdf8bb6719d3d044c71a2a8ceab85ccef8f1))
|
||||
|
||||
### 📦 依赖升级
|
||||
|
||||
- Graceful Response 4.0.1-boot3 => 5.0.0-boot3 ([2208dbd](https://github.com/continew-org/continew-starter/commit/2208dbd0282964aab76b02e811d6689ba69d75fd))
|
||||
- Snail Job 1.1.0 => 1.1.2
|
||||
- Sa Token 1.38.0 => 1.39.0
|
||||
- MyBatis Flex 1.9.3 => 1.9.7
|
||||
- Redisson 3.32.0 => 3.35.0
|
||||
- Cos ID 2.9.1 => 2.9.6
|
||||
- SMS4J 3.2.1 => 3.3.2
|
||||
- X File Storage 2.2.0 => 2.2.1
|
||||
- Hutool 5.8.32 => 5.8.29
|
||||
- aws-java-sdk-s3 1.12.761 => 1.12.771
|
||||
- snakeyaml 2.2 => 2.3
|
||||
|
||||
## [v2.5.2](https://github.com/continew-org/continew-starter/compare/v2.5.1...v2.5.2) (2024-08-14)
|
||||
|
||||
### 💎 功能优化
|
||||
|
||||
- 【api-doc】重构接口文档枚举展示处理 ([bf51837](https://github.com/continew-org/continew-starter/commit/bf51837991746c5576d7baffcc2296fe8ca91586)) ([4c4f98a](https://github.com/continew-org/continew-starter/commit/4c4f98a86e6d10b2b52814406bd0dc3d248c8486))
|
||||
- 【web】针对最新响应风格增加全局响应格式 ([bf51837](https://github.com/continew-org/continew-starter/commit/bf51837991746c5576d7baffcc2296fe8ca91586)) ([4c4f98a](https://github.com/continew-org/continew-starter/commit/4c4f98a86e6d10b2b52814406bd0dc3d248c8486))
|
||||
|
||||
### 🐛 问题修复
|
||||
|
||||
- 【extension/crud】重构排序字段处理,预防 SQL 注入问题 ([c31fa75](https://github.com/continew-org/continew-starter/commit/c31fa753b6d9753d72a2e28bf3184981ed848ac2)) ([22ebdfe](https://github.com/continew-org/continew-starter/commit/22ebdfeb9f30a8832825424a05343acfaf31643b))
|
||||
- 【security/crypto】修复 updateById 修改未正确加密的问题 ([b0a2a8c](https://github.com/continew-org/continew-starter/commit/b0a2a8c927171795e3ed89a820caf829f76c80ee))
|
||||
|
||||
## [v2.5.1](https://github.com/continew-org/continew-starter/compare/v2.5.0...v2.5.1) (2024-08-12)
|
||||
|
||||
### 🐛 问题修复
|
||||
|
||||
- 【data】移除 SQL 函数接口中的 SQL 拼接 ([6693cd4](https://github.com/continew-org/continew-starter/commit/6693cd49b93244b42dfadd4f4be28e526d764425))
|
||||
|
||||
## [v2.5.0](https://github.com/continew-org/continew-starter/compare/v2.4.0...v2.5.0) (2024-08-07)
|
||||
|
||||
### ✨ 新特性
|
||||
|
||||
141
README.md
141
README.md
@@ -72,6 +72,11 @@ ContiNew Starter 就是将脚手架项目中的通用基础配置进行了封装
|
||||
|
||||
第一种方式:如您使用的是 Spring Boot Parent 的方式,则替换 Spring Boot Parent 为 ContiNew Starter
|
||||
|
||||
> 最新稳定版(latest-version)
|
||||
<a href="https://central.sonatype.com/search?q=continew-starter" target="_blank" rel="noopener" style="display: inline-block;">
|
||||
<img src="https://img.shields.io/maven-central/v/top.continew/continew-starter.svg?label=Maven%20Central&logo=sonatype&logoColor=FFF" alt="Release" />
|
||||
</a>
|
||||
|
||||
```xml
|
||||
<parent>
|
||||
<groupId>top.continew</groupId>
|
||||
@@ -137,100 +142,48 @@ continew-starter.web:
|
||||
|
||||
## 模块结构
|
||||
|
||||
### 核心模块
|
||||
|
||||
| 模块名称 | 模块说明 | 依赖版本 |
|
||||
| --------------------- | ------------------------------------ |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| continew-starter-core | 核心模块:包含线程池、项目等自动配置 | <a href="https://spring.io/projects/spring-boot" target="_blank">Spring Boot</a>:3.1.11<br /><a href="https://www.hutool.cn/" target="_blank">Hutool</a>:5.8.29<br />mica-ip2region:3.2.6 |
|
||||
|
||||
### JSON模块
|
||||
|
||||
| 模块名称 | 模块说明 |
|
||||
| ----------------------------- | -------------------- |
|
||||
| continew-starter-json-jackson | Jackson 序列化等配置 |
|
||||
|
||||
### 接口文档
|
||||
|
||||
| 模块名称 | 模块说明 |
|
||||
| ------------------------ | ---------------- |
|
||||
| continew-starter-api-doc | Knife4j 自动配置 |
|
||||
|
||||
### 安全模块
|
||||
|
||||
| 模块名称 | 模块说明 |
|
||||
| ---------------------------------- | ----------------- |
|
||||
| continew-starter-security-password | 密码编码器 |
|
||||
| continew-starter-security-mask | JSON 脱敏 |
|
||||
| continew-starter-security-crypto | 数据库字段加/解密 |
|
||||
| continew-starter-security-limiter | 限流器 |
|
||||
|
||||
### Web模块
|
||||
|
||||
| 模块名称 | 模块说明 |
|
||||
| -------------------- | ---------------------------------- |
|
||||
| continew-starter-web | 跨域、全局异常、错误处理等自动配置 |
|
||||
|
||||
### 日志模块
|
||||
|
||||
| 模块名称 | 模块说明 |
|
||||
|----------------------------------| ----------------------------------------- |
|
||||
| continew-starter-log-core | 日志核心模块 |
|
||||
| continew-starter-log-interceptor | 拦截器版(Spring Boot Actuator HttpTrace 增强版) |
|
||||
|
||||
### 存储模块
|
||||
|
||||
| 模块名称 | 模块说明 |
|
||||
| ------------------------------ | -------- |
|
||||
| continew-starter-storage-local | 本地存储 |
|
||||
|
||||
### 文件处理模块
|
||||
|
||||
| 模块名称 | 模块说明 |
|
||||
| --------------------------- | -------------- |
|
||||
| continew-starter-file-excel | Excel 相关配置 |
|
||||
|
||||
### 验证码模块
|
||||
|
||||
| 模块名称 | 模块说明 |
|
||||
| --------------------------------- | ---------- |
|
||||
| continew-starter-captcha-graphic | 图形验证码 |
|
||||
| continew-starter-captcha-behavior | 行为验证码 |
|
||||
|
||||
### 缓存模块
|
||||
|
||||
| 模块名称 | 模块说明 |
|
||||
| ---------------------------------- | --------------------- |
|
||||
| continew-starter-cache-redisson | Redisson 自动配置 |
|
||||
| continew-starter-cache-springcache | Spring Cache 自动配置 |
|
||||
| continew-starter-cache-jetcache | JetCache 自动配置 |
|
||||
|
||||
### 数据访问模块
|
||||
|
||||
| 模块名称 | 模块说明 |
|
||||
| ---------------------------------- | --------------------- |
|
||||
| continew-starter-data-core | 数据访问核心模块 |
|
||||
| continew-starter-data-mybatis-plus | MyBatis Plus 自动配置 |
|
||||
| continew-starter-data-mybatis-flex | MyBatis Flex 自动配置 |
|
||||
|
||||
### 认证模块
|
||||
|
||||
| 模块名称 | 模块说明 |
|
||||
| ------------------------------ | ----------------- |
|
||||
| continew-starter-auth-satoken | SaToken 自动配置 |
|
||||
| continew-starter-auth-justauth | JustAuth 自动配置 |
|
||||
|
||||
### 消息模块
|
||||
|
||||
| 模块名称 | 模块说明 |
|
||||
| ------------------------------------ | --------- |
|
||||
| continew-starter-messaging-mail | 邮件 |
|
||||
| continew-starter-messaging-websocket | WebSocket |
|
||||
|
||||
### 扩展模块
|
||||
|
||||
| 模块名称 | 模块说明 |
|
||||
| ------------------------------- | --------------------------------------------- |
|
||||
| continew-starter-extension-crud | 扩展模块:BaseController 自定义 CRUD API 封装 |
|
||||
```
|
||||
continew-starter
|
||||
├─ continew-starter-core(核心模块:包含线程池等自动配置)
|
||||
├─ continew-starter-json(JSON 模块)
|
||||
│ └─ continew-starter-json-jackson
|
||||
├─ continew-starter-api-doc(接口文档模块:Spring Doc + Knife4j)
|
||||
├─ continew-starter-web(Web 开发模块:包含跨域、全局异常+响应、链路追踪等自动配置)
|
||||
├─ continew-starter-auth(认证模块)
|
||||
│ ├─ continew-starter-auth-satoken(国产轻量认证鉴权)
|
||||
│ └─ continew-starter-auth-justauth(第三方登录)
|
||||
├─ continew-starter-data(数据访问模块)
|
||||
│ ├─ continew-starter-data-core(通用模块)
|
||||
│ ├─ continew-starter-data-mp(MyBatis Plus)
|
||||
│ └─ continew-starter-data-mf(MyBatis Flex)
|
||||
├─ continew-starter-cache(缓存模块)
|
||||
│ ├─ continew-starter-cache-redisson(Redisson)
|
||||
│ ├─ continew-starter-cache-jetcache(JetCache 多级缓存)
|
||||
│ └─ continew-starter-cache-springcache(Spring 缓存)
|
||||
├─ continew-starter-security(安全模块)
|
||||
│ ├─ continew-starter-security-crypto(加密:字段加解密)
|
||||
│ ├─ continew-starter-security-mask(脱敏:JSON 数据脱敏)
|
||||
│ ├─ continew-starter-security-limiter(限流)
|
||||
│ └─ continew-starter-security-password(密码编码器)
|
||||
├─ continew-starter-captcha(验证码模块)
|
||||
│ ├─ continew-starter-captcha-graphic(静态验证码)
|
||||
│ └─ continew-starter-captcha-behavior(动态验证码)
|
||||
├─ continew-starter-messaging(消息模块)
|
||||
│ ├─ continew-starter-messaging-mail(邮件)
|
||||
│ └─ continew-starter-messaging-websocket(WebSocket)
|
||||
├─ continew-starter-log(日志模块)
|
||||
│ ├─ continew-starter-log-core(通用模块)
|
||||
│ └─ continew-starter-log-interceptor(拦截器版(Spring Boot Actuator HttpTrace 增强版))
|
||||
├─ continew-starter-file(文件处理模块)
|
||||
│ └─ continew-starter-file-excel(Easy Excel)
|
||||
├─ continew-starter-storage(存储模块)
|
||||
│ └─ continew-starter-storage-local(本地存储)
|
||||
└─ continew-starter-extension(扩展模块)
|
||||
└─ continew-starter-extension-crud(CRUD 模块)
|
||||
├─ continew-starter-extension-crud-core(通用模块)
|
||||
├─ continew-starter-extension-crud-mp(MyBatis Plus)
|
||||
└─ continew-starter-extension-crud-mf(MyBatis Flex)
|
||||
```
|
||||
|
||||
## 贡献代码
|
||||
|
||||
|
||||
@@ -17,16 +17,11 @@
|
||||
package top.continew.starter.apidoc.autoconfigure;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fasterxml.jackson.databind.type.CollectionType;
|
||||
import com.fasterxml.jackson.databind.type.SimpleType;
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.media.Schema;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
@@ -48,15 +43,13 @@ import org.springframework.http.CacheControl;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
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.apidoc.util.EnumTypeUtils;
|
||||
import top.continew.starter.core.autoconfigure.project.ProjectProperties;
|
||||
import top.continew.starter.core.enums.BaseEnum;
|
||||
import top.continew.starter.core.util.GeneralPropertySourceFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* API 文档自动配置
|
||||
@@ -156,95 +149,14 @@ public class SpringDocAutoConfiguration implements WebMvcConfigurer {
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义参数配置(针对 BaseEnum 展示枚举值和描述)
|
||||
* 自定义 BaseEnum 枚举参数配置(针对实现了 BaseEnum 的枚举,优化其枚举值和描述展示)
|
||||
*
|
||||
* @return {@link BaseEnumParameterHandler }
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@Bean
|
||||
public ParameterCustomizer customParameterCustomizer() {
|
||||
return (parameterModel, methodParameter) -> {
|
||||
Class<?> parameterType = methodParameter.getParameterType();
|
||||
// 判断是否为 BaseEnum 的子类型
|
||||
if (!ClassUtil.isAssignable(BaseEnum.class, parameterType)) {
|
||||
return parameterModel;
|
||||
}
|
||||
String description = parameterModel.getDescription();
|
||||
if (StrUtil.contains(description, "color:red")) {
|
||||
return parameterModel;
|
||||
}
|
||||
// 封装参数配置
|
||||
this.configureSchema(parameterModel.getSchema(), parameterType);
|
||||
// 自定义枚举描述
|
||||
parameterModel.setDescription(description + "<span style='color:red'>" + this
|
||||
.getDescMap(parameterType) + "</span>");
|
||||
return parameterModel;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义参数配置(针对 BaseEnum 展示枚举值和描述)
|
||||
*
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@Bean
|
||||
public PropertyCustomizer customPropertyCustomizer() {
|
||||
return (schema, type) -> {
|
||||
Class<?> rawClass;
|
||||
// 获取原始类的类型
|
||||
if (type.getType() instanceof SimpleType) {
|
||||
rawClass = ((SimpleType)type.getType()).getRawClass();
|
||||
} else if (type.getType() instanceof CollectionType) {
|
||||
rawClass = ((CollectionType)type.getType()).getContentType().getRawClass();
|
||||
} else {
|
||||
rawClass = Object.class;
|
||||
}
|
||||
// 判断是否为 BaseEnum 的子类型
|
||||
if (!ClassUtil.isAssignable(BaseEnum.class, rawClass)) {
|
||||
return schema;
|
||||
}
|
||||
// 封装参数配置
|
||||
this.configureSchema(schema, rawClass);
|
||||
// 自定义参数描述
|
||||
schema.setDescription(schema.getDescription() + "<span style='color:red'>" + this
|
||||
.getDescMap(rawClass) + "</span>");
|
||||
return schema;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 封装 Schema 配置
|
||||
*
|
||||
* @param schema Schema
|
||||
* @param enumClass 枚举类型
|
||||
* @since 2.4.0
|
||||
*/
|
||||
private void configureSchema(Schema schema, Class<?> enumClass) {
|
||||
BaseEnum[] enums = (BaseEnum[])enumClass.getEnumConstants();
|
||||
// 设置枚举可用值
|
||||
List<String> valueList = Arrays.stream(enums).map(e -> e.getValue().toString()).toList();
|
||||
schema.setEnum(valueList);
|
||||
// 设置枚举值类型和格式
|
||||
String enumValueType = EnumTypeUtils.getEnumValueTypeAsString(enumClass);
|
||||
schema.setType(enumValueType);
|
||||
switch (enumValueType) {
|
||||
case "integer" -> schema.setFormat("int32");
|
||||
case "long" -> schema.setFormat("int64");
|
||||
case "number" -> schema.setFormat("double");
|
||||
default -> schema.setFormat(enumValueType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取枚举描述 Map
|
||||
*
|
||||
* @param enumClass 枚举类型
|
||||
* @return 枚举描述 Map
|
||||
* @since 2.4.0
|
||||
*/
|
||||
private Map<Object, String> getDescMap(Class<?> enumClass) {
|
||||
BaseEnum[] enums = (BaseEnum[])enumClass.getEnumConstants();
|
||||
return Arrays.stream(enums)
|
||||
.collect(Collectors.toMap(BaseEnum::getValue, BaseEnum::getDescription, (a, b) -> a, LinkedHashMap::new));
|
||||
public BaseEnumParameterHandler customParameterCustomizer() {
|
||||
return new BaseEnumParameterHandler();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.apidoc.handler;
|
||||
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fasterxml.jackson.databind.type.CollectionType;
|
||||
import com.fasterxml.jackson.databind.type.SimpleType;
|
||||
import io.swagger.v3.core.converter.AnnotatedType;
|
||||
import io.swagger.v3.oas.models.media.Schema;
|
||||
import io.swagger.v3.oas.models.parameters.Parameter;
|
||||
import org.springdoc.core.customizers.ParameterCustomizer;
|
||||
import org.springdoc.core.customizers.PropertyCustomizer;
|
||||
import org.springframework.core.MethodParameter;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import top.continew.starter.apidoc.util.DocUtils;
|
||||
import top.continew.starter.core.enums.BaseEnum;
|
||||
|
||||
/**
|
||||
* 自定义 BaseEnum 枚举参数处理器
|
||||
* <p>
|
||||
* 针对实现了 BaseEnum 的枚举,优化其枚举值和描述展示
|
||||
* </p>
|
||||
*
|
||||
* @author echo
|
||||
* @since 2.5.2
|
||||
*/
|
||||
public class BaseEnumParameterHandler implements ParameterCustomizer, PropertyCustomizer {
|
||||
|
||||
@Override
|
||||
public Parameter customize(Parameter parameterModel, MethodParameter methodParameter) {
|
||||
Class<?> parameterType = methodParameter.getParameterType();
|
||||
// 判断是否为 BaseEnum 的子类型
|
||||
if (!ClassUtil.isAssignable(BaseEnum.class, parameterType)) {
|
||||
return parameterModel;
|
||||
}
|
||||
String description = parameterModel.getDescription();
|
||||
if (StrUtil.contains(description, "color:red")) {
|
||||
return parameterModel;
|
||||
}
|
||||
// 自定义枚举描述并封装参数配置
|
||||
configureSchema(parameterModel.getSchema(), parameterType);
|
||||
parameterModel.setDescription(appendEnumDescription(description, parameterType));
|
||||
return parameterModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Schema customize(Schema schema, AnnotatedType type) {
|
||||
Class<?> rawClass = resolveRawClass(type.getType());
|
||||
// 判断是否为 BaseEnum 的子类型
|
||||
if (!ClassUtil.isAssignable(BaseEnum.class, rawClass)) {
|
||||
return schema;
|
||||
}
|
||||
// 自定义参数描述并封装参数配置
|
||||
configureSchema(schema, rawClass);
|
||||
schema.setDescription(appendEnumDescription(schema.getDescription(), rawClass));
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 封装 Schema 配置
|
||||
*
|
||||
* @param schema Schema
|
||||
* @param enumClass 枚举类型
|
||||
*/
|
||||
private void configureSchema(Schema schema, Class<?> enumClass) {
|
||||
BaseEnum[] enums = (BaseEnum[])enumClass.getEnumConstants();
|
||||
List<String> valueList = Arrays.stream(enums).map(e -> e.getValue().toString()).toList();
|
||||
schema.setEnum(valueList);
|
||||
String enumValueType = DocUtils.getEnumValueTypeAsString(enumClass);
|
||||
schema.setType(enumValueType);
|
||||
schema.setFormat(DocUtils.resolveFormat(enumValueType));
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加枚举描述
|
||||
*
|
||||
* @param originalDescription 原始描述
|
||||
* @param enumClass 枚举类型
|
||||
* @return 追加后的描述字符串
|
||||
*/
|
||||
private String appendEnumDescription(String originalDescription, Class<?> enumClass) {
|
||||
return originalDescription + "<span style='color:red'>" + DocUtils.getDescMap(enumClass) + "</span>";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析原始类
|
||||
*
|
||||
* @param type 类型
|
||||
* @return 原始类的 Class 对象
|
||||
*/
|
||||
private Class<?> resolveRawClass(Type type) {
|
||||
if (type instanceof SimpleType simpleType) {
|
||||
return simpleType.getRawClass();
|
||||
} else if (type instanceof CollectionType collectionType) {
|
||||
return collectionType.getContentType().getRawClass();
|
||||
} else {
|
||||
return Object.class;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.apidoc.util;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import top.continew.starter.core.enums.BaseEnum;
|
||||
|
||||
/**
|
||||
* 接口文档工具类
|
||||
*
|
||||
* @author echo
|
||||
* @since 2.5.2
|
||||
*/
|
||||
public class DocUtils {
|
||||
|
||||
private DocUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取枚举值类型
|
||||
*
|
||||
* @param enumClass 枚举类型
|
||||
* @return 枚举值类型
|
||||
*/
|
||||
public static String getEnumValueTypeAsString(Class<?> enumClass) {
|
||||
// 获取枚举类实现的所有接口
|
||||
Type[] interfaces = enumClass.getGenericInterfaces();
|
||||
// 定义枚举值类型的映射
|
||||
Map<Class<?>, String> typeMap = Map
|
||||
.of(Integer.class, "integer", Long.class, "long", Double.class, "number", String.class, "string");
|
||||
// 遍历所有接口
|
||||
for (Type type : interfaces) {
|
||||
// 检查接口是否为参数化类型并且原始类型为 BaseEnum
|
||||
if (type instanceof ParameterizedType parameterizedType && parameterizedType
|
||||
.getRawType() == BaseEnum.class) {
|
||||
Type actualType = parameterizedType.getActualTypeArguments()[0];
|
||||
// 检查实际类型参数是否为类类型,并返回对应的字符串类型
|
||||
if (actualType instanceof Class<?> actualClass) {
|
||||
return typeMap.getOrDefault(actualClass, "string");
|
||||
}
|
||||
}
|
||||
}
|
||||
// 默认返回 "string" 类型
|
||||
return "string";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析枚举值的格式
|
||||
*
|
||||
* @param enumValueType 枚举值类型
|
||||
* @return String 格式化类型
|
||||
*/
|
||||
public static String resolveFormat(String enumValueType) {
|
||||
return switch (enumValueType) {
|
||||
case "integer" -> "int32";
|
||||
case "long" -> "int64";
|
||||
case "number" -> "double";
|
||||
default -> enumValueType;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 具有 RestController 注释,既检查是否继承了BaseController
|
||||
*
|
||||
* @param clazz clazz
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean hasRestControllerAnnotation(Class<?> clazz) {
|
||||
// 如果注释包含 RestController 注解,则返回 true
|
||||
if (clazz.isAnnotationPresent(RestController.class)) {
|
||||
return true;
|
||||
}
|
||||
// 递归检查父类
|
||||
Class<?> superClass = clazz.getSuperclass();
|
||||
// 循环检查父类
|
||||
while (superClass != null && !superClass.equals(Object.class)) {
|
||||
// 如果父类包含 RestController 注解,则返回 true
|
||||
if (hasRestControllerAnnotation(superClass)) {
|
||||
return true;
|
||||
}
|
||||
// 递归检查接口
|
||||
superClass = superClass.getSuperclass();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取枚举描述 Map
|
||||
*
|
||||
* @param enumClass 枚举类型
|
||||
* @return 枚举描述 Map
|
||||
*/
|
||||
public static Map<Object, String> getDescMap(Class<?> enumClass) {
|
||||
BaseEnum[] enums = (BaseEnum[])enumClass.getEnumConstants();
|
||||
return Arrays.stream(enums)
|
||||
.collect(Collectors.toMap(BaseEnum::getValue, BaseEnum::getDescription, (a, b) -> a, LinkedHashMap::new));
|
||||
}
|
||||
}
|
||||
@@ -1,73 +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.apidoc.util;
|
||||
|
||||
import top.continew.starter.core.enums.BaseEnum;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
/**
|
||||
* 枚举类型工具
|
||||
*
|
||||
* @author echo
|
||||
* @since 2.4.0
|
||||
*/
|
||||
public class EnumTypeUtils {
|
||||
|
||||
private EnumTypeUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取枚举值类型
|
||||
*
|
||||
* @param enumClass 枚举类型
|
||||
* @return 枚举值类型
|
||||
*/
|
||||
public static String getEnumValueTypeAsString(Class<?> enumClass) {
|
||||
try {
|
||||
// 获取枚举类实现的所有接口
|
||||
Type[] interfaces = enumClass.getGenericInterfaces();
|
||||
// 遍历所有接口
|
||||
for (Type type : interfaces) {
|
||||
// 检查接口是否为参数化类型
|
||||
if (type instanceof ParameterizedType parameterizedType) {
|
||||
// 检查接口的原始类型是否为 BaseEnum
|
||||
if (parameterizedType.getRawType() != BaseEnum.class) {
|
||||
continue;
|
||||
}
|
||||
Type actualType = parameterizedType.getActualTypeArguments()[0];
|
||||
// 检查实际类型参数是否为类类型
|
||||
if (actualType instanceof Class<?> actualClass) {
|
||||
if (actualClass == Integer.class) {
|
||||
return "integer";
|
||||
} else if (actualClass == Long.class) {
|
||||
return "long";
|
||||
} else if (actualClass == Double.class) {
|
||||
return "number";
|
||||
} else if (actualClass == String.class) {
|
||||
return "string";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// ignored
|
||||
}
|
||||
return "string";
|
||||
}
|
||||
}
|
||||
@@ -272,6 +272,11 @@ public class StringConstants {
|
||||
*/
|
||||
public static final String ROUND_BRACKET_END = ")";
|
||||
|
||||
/**
|
||||
* 等号(=)
|
||||
*/
|
||||
public static final String EQUALS = "=";
|
||||
|
||||
/**
|
||||
* 路径模式
|
||||
*/
|
||||
|
||||
@@ -18,5 +18,10 @@
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-db</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-commons</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.data.core.util;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* SQL 注入验证工具类
|
||||
*
|
||||
* @author hubin(<a href="https://github.com/baomidou/mybatis-plus">MyBatis Plus</a>)
|
||||
* @author zhoujf(<a href="https://github.com/jeecgboot/JeecgBoot">JeecgBoot</a>)
|
||||
* @author Charles7c
|
||||
* @since 2.5.2
|
||||
*/
|
||||
public class SqlInjectionUtils {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SqlInjectionUtils.class);
|
||||
|
||||
/**
|
||||
* SQL语法检查正则:符合两个关键字(有先后顺序)才算匹配
|
||||
*/
|
||||
private static final Pattern SQL_SYNTAX_PATTERN = Pattern
|
||||
.compile("(insert|delete|update|select|create|drop|truncate|grant|alter|deny|revoke|call|execute|exec|declare|show|rename|set)" + "\\s+.*(into|from|set|where|table|database|view|index|on|cursor|procedure|trigger|for|password|union|and|or)|(select\\s*\\*\\s*from\\s+)|(and|or)\\s+.*", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 使用'、;或注释截断SQL检查正则
|
||||
*/
|
||||
private static final Pattern SQL_COMMENT_PATTERN = Pattern
|
||||
.compile("'.*(or|union|--|#|/\\*|;)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* SQL 语法关键字
|
||||
*/
|
||||
private static final String SQL_SYNTAX_KEYWORD = "and |exec |peformance_schema|information_schema|extractvalue|updatexml|geohash|gtid_subset|gtid_subtract|insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |;|or |+|--";
|
||||
|
||||
/**
|
||||
* SQL 函数检查正则
|
||||
*/
|
||||
private static final String[] SQL_FUNCTION_PATTERN = new String[] {"chr\\s*\\(", "mid\\s*\\(", " char\\s*\\(",
|
||||
"sleep\\s*\\(", "user\\s*\\(", "show\\s+tables", "user[\\s]*\\([\\s]*\\)", "show\\s+databases",
|
||||
"sleep\\(\\d*\\)", "sleep\\(.*\\)",};
|
||||
|
||||
private static final String MESSAGE_TEMPLATE = "SQL 注入检查: 检查值=>{}<=存在 SQL 注入关键字, 关键字=>{}<=";
|
||||
|
||||
private SqlInjectionUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查参数是否存在 SQL 注入
|
||||
*
|
||||
* @param value 检查参数
|
||||
* @return true:非法;false:合法
|
||||
*/
|
||||
public static boolean check(String value) {
|
||||
return check(value, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查参数是否存在 SQL 注入
|
||||
*
|
||||
* @param value 检查参数
|
||||
* @param customKeyword 自定义关键字
|
||||
* @return true:非法;false:合法
|
||||
*/
|
||||
public static boolean check(String value, String customKeyword) {
|
||||
if (CharSequenceUtil.isBlank(value)) {
|
||||
return false;
|
||||
}
|
||||
// 处理是否包含 SQL 注释字符 || 检查是否包含 SQL 注入敏感字符
|
||||
if (SQL_COMMENT_PATTERN.matcher(value).find() || SQL_SYNTAX_PATTERN.matcher(value).find()) {
|
||||
log.warn("SQL 注入检查: 检查值=>{}<=存在 SQL 注释字符或 SQL 注入敏感字符", value);
|
||||
return true;
|
||||
}
|
||||
// 转换成小写再进行比较
|
||||
value = value.toLowerCase().trim();
|
||||
// 检查是否包含 SQL 语法关键字
|
||||
if (checkKeyword(value, SQL_SYNTAX_KEYWORD.split("\\|"))) {
|
||||
return true;
|
||||
}
|
||||
// 检查是否包含自定义关键字
|
||||
if (CharSequenceUtil.isNotBlank(customKeyword) && checkKeyword(value, customKeyword.split("\\|"))) {
|
||||
return true;
|
||||
}
|
||||
// 检查是否包含 SQL 注入敏感字符
|
||||
for (String pattern : SQL_FUNCTION_PATTERN) {
|
||||
if (Pattern.matches(".*" + pattern + ".*", value)) {
|
||||
log.warn(MESSAGE_TEMPLATE, value, pattern);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查参数是否存在关键字
|
||||
*
|
||||
* @param value 检查参数
|
||||
* @param keywords 关键字列表
|
||||
* @return true:非法;false:合法
|
||||
*/
|
||||
private static boolean checkKeyword(String value, String[] keywords) {
|
||||
for (String keyword : keywords) {
|
||||
if (value.contains(keyword)) {
|
||||
log.warn(MESSAGE_TEMPLATE, value, keyword);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>continew-starter-data-mybatis-flex</artifactId>
|
||||
<artifactId>continew-starter-data-mf</artifactId>
|
||||
<description>ContiNew Starter 数据访问模块 - MyBatis Flex</description>
|
||||
|
||||
<dependencies>
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.autoconfigure;
|
||||
package top.continew.starter.data.mf.autoconfigure;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.autoconfigure;
|
||||
package top.continew.starter.data.mf.autoconfigure;
|
||||
|
||||
import com.mybatisflex.core.dialect.DbType;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.autoconfigure;
|
||||
package top.continew.starter.data.mf.autoconfigure;
|
||||
|
||||
import com.mybatisflex.core.dialect.DbType;
|
||||
import com.mybatisflex.core.dialect.DialectFactory;
|
||||
@@ -30,8 +30,8 @@ import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.continew.starter.core.util.GeneralPropertySourceFactory;
|
||||
import top.continew.starter.data.mybatis.flex.datapermission.DataPermissionDialect;
|
||||
import top.continew.starter.data.mybatis.flex.datapermission.DataPermissionFilter;
|
||||
import top.continew.starter.data.mf.datapermission.DataPermissionDialect;
|
||||
import top.continew.starter.data.mf.datapermission.DataPermissionFilter;
|
||||
|
||||
/**
|
||||
* MyBatis Flex 自动配置
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.base;
|
||||
package top.continew.starter.data.mf.base;
|
||||
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.base;
|
||||
package top.continew.starter.data.mf.base;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@@ -14,9 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.datapermission;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
package top.continew.starter.data.mf.datapermission;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@@ -31,16 +29,9 @@ import java.lang.annotation.*;
|
||||
@Documented
|
||||
public @interface DataPermission {
|
||||
|
||||
/**
|
||||
* Alias for the {@link #tableAlias()} attribute.
|
||||
*/
|
||||
@AliasFor("tableAlias")
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* 表别名
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String tableAlias() default "";
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.datapermission;
|
||||
package top.continew.starter.data.mf.datapermission;
|
||||
|
||||
import org.aspectj.lang.annotation.*;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.datapermission;
|
||||
package top.continew.starter.data.mf.datapermission;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.datapermission;
|
||||
package top.continew.starter.data.mf.datapermission;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.mybatisflex.core.dialect.impl.CommonsDialectImpl;
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.datapermission;
|
||||
package top.continew.starter.data.mf.datapermission;
|
||||
|
||||
/**
|
||||
* 数据权限过滤器接口
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.datapermission;
|
||||
package top.continew.starter.data.mf.datapermission;
|
||||
|
||||
/**
|
||||
* 数据权限枚举
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.service;
|
||||
package top.continew.starter.data.mf.service;
|
||||
|
||||
/**
|
||||
* 通用业务接口
|
||||
@@ -14,11 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.service.impl;
|
||||
package top.continew.starter.data.mf.service.impl;
|
||||
|
||||
import top.continew.starter.core.util.ClassUtils;
|
||||
import top.continew.starter.data.mybatis.flex.base.BaseMapper;
|
||||
import top.continew.starter.data.mybatis.flex.service.IService;
|
||||
import top.continew.starter.data.mf.base.BaseMapper;
|
||||
import top.continew.starter.data.mf.service.IService;
|
||||
|
||||
/**
|
||||
* 通用业务实现类
|
||||
@@ -14,21 +14,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.flex.query;
|
||||
package top.continew.starter.data.mf.util;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import top.continew.starter.core.exception.BadRequestException;
|
||||
import top.continew.starter.core.util.ReflectUtils;
|
||||
import top.continew.starter.core.util.validate.ValidationUtils;
|
||||
import top.continew.starter.data.core.annotation.Query;
|
||||
import top.continew.starter.data.core.annotation.QueryIgnore;
|
||||
import top.continew.starter.data.core.enums.QueryType;
|
||||
import top.continew.starter.data.core.util.SqlInjectionUtils;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
@@ -56,10 +58,9 @@ public class QueryWrapperHelper {
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @param <Q> 查询条件数据类型
|
||||
* @param <R> 查询数据类型
|
||||
* @return QueryWrapper
|
||||
*/
|
||||
public static <Q, R> QueryWrapper build(Q query) {
|
||||
public static <Q> QueryWrapper build(Q query) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
// 没有查询条件,直接返回
|
||||
if (null == query) {
|
||||
@@ -70,6 +71,34 @@ public class QueryWrapperHelper {
|
||||
return build(query, fieldList, queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 QueryWrapper
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @param sort 排序条件
|
||||
* @param <Q> 查询条件数据类型
|
||||
* @return QueryWrapper
|
||||
* @since 2.5.2
|
||||
*/
|
||||
public static <Q> QueryWrapper build(Q query, Sort sort) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
// 没有查询条件,直接返回
|
||||
if (null == query) {
|
||||
return queryWrapper;
|
||||
}
|
||||
// 设置排序条件
|
||||
if (sort != null && sort.isSorted()) {
|
||||
for (Sort.Order order : sort) {
|
||||
String field = CharSequenceUtil.toUnderlineCase(order.getProperty());
|
||||
ValidationUtils.throwIf(SqlInjectionUtils.check(field), "排序字段包含非法字符");
|
||||
queryWrapper.orderBy(field, order.isAscending());
|
||||
}
|
||||
}
|
||||
// 获取查询条件中所有的字段
|
||||
List<Field> fieldList = ReflectUtils.getNonStaticFields(query.getClass());
|
||||
return build(query, fieldList, queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 QueryWrapper
|
||||
*
|
||||
@@ -0,0 +1 @@
|
||||
top.continew.starter.data.mf.autoconfigure.MybatisFlexAutoConfiguration
|
||||
@@ -9,7 +9,7 @@
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>continew-starter-data-mybatis-plus</artifactId>
|
||||
<artifactId>continew-starter-data-mp</artifactId>
|
||||
<description>ContiNew Starter 数据访问模块 - MyBatis Plus</description>
|
||||
|
||||
<dependencies>
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.autoconfigure;
|
||||
package top.continew.starter.data.mp.autoconfigure;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
@@ -14,12 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.autoconfigure;
|
||||
package top.continew.starter.data.mp.autoconfigure;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import top.continew.starter.data.mybatis.plus.autoconfigure.idgenerator.MyBatisPlusIdGeneratorProperties;
|
||||
import top.continew.starter.data.mp.autoconfigure.idgenerator.MyBatisPlusIdGeneratorProperties;
|
||||
|
||||
/**
|
||||
* MyBatis Plus 扩展配置属性
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.autoconfigure;
|
||||
package top.continew.starter.data.mp.autoconfigure;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusPropertiesCustomizer;
|
||||
@@ -22,6 +22,7 @@ import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
@@ -38,10 +39,12 @@ import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.continew.starter.core.util.GeneralPropertySourceFactory;
|
||||
import top.continew.starter.data.mybatis.plus.autoconfigure.idgenerator.MyBatisPlusIdGeneratorConfiguration;
|
||||
import top.continew.starter.data.mybatis.plus.datapermission.DataPermissionFilter;
|
||||
import top.continew.starter.data.mybatis.plus.datapermission.DataPermissionHandlerImpl;
|
||||
import top.continew.starter.data.mybatis.plus.handler.MybatisBaseEnumTypeHandler;
|
||||
import top.continew.starter.data.mp.autoconfigure.idgenerator.MyBatisPlusIdGeneratorConfiguration;
|
||||
import top.continew.starter.data.mp.datapermission.DataPermissionFilter;
|
||||
import top.continew.starter.data.mp.datapermission.DataPermissionHandlerImpl;
|
||||
import top.continew.starter.data.mp.handler.MybatisBaseEnumTypeHandler;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MyBatis Plus 自动配置
|
||||
@@ -76,6 +79,11 @@ public class MybatisPlusAutoConfiguration {
|
||||
@ConditionalOnMissingBean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor(MyBatisPlusExtensionProperties properties) {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 其他拦截器
|
||||
Map<String, InnerInterceptor> innerInterceptors = SpringUtil.getBeansOfType(InnerInterceptor.class);
|
||||
if (!innerInterceptors.isEmpty()) {
|
||||
innerInterceptors.values().forEach(interceptor::addInnerInterceptor);
|
||||
}
|
||||
// 数据权限插件
|
||||
MyBatisPlusExtensionProperties.DataPermissionProperties dataPermissionProperties = properties
|
||||
.getDataPermission();
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.autoconfigure.idgenerator;
|
||||
package top.continew.starter.data.mp.autoconfigure.idgenerator;
|
||||
|
||||
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
|
||||
import me.ahoo.cosid.snowflake.SnowflakeId;
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.autoconfigure.idgenerator;
|
||||
package top.continew.starter.data.mp.autoconfigure.idgenerator;
|
||||
|
||||
import cn.hutool.core.net.NetUtil;
|
||||
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.autoconfigure.idgenerator;
|
||||
package top.continew.starter.data.mp.autoconfigure.idgenerator;
|
||||
|
||||
import top.continew.starter.data.mybatis.plus.enums.MyBatisPlusIdGeneratorType;
|
||||
import top.continew.starter.data.mp.enums.MyBatisPlusIdGeneratorType;
|
||||
|
||||
/**
|
||||
* MyBatis ID 生成器配置属性
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.base;
|
||||
package top.continew.starter.data.mp.base;
|
||||
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
|
||||
@@ -14,9 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.datapermission;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
package top.continew.starter.data.mp.datapermission;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@@ -31,16 +29,9 @@ import java.lang.annotation.*;
|
||||
@Documented
|
||||
public @interface DataPermission {
|
||||
|
||||
/**
|
||||
* Alias for the {@link #tableAlias()} attribute.
|
||||
*/
|
||||
@AliasFor("tableAlias")
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* 表别名
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String tableAlias() default "";
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.datapermission;
|
||||
package top.continew.starter.data.mp.datapermission;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.datapermission;
|
||||
package top.continew.starter.data.mp.datapermission;
|
||||
|
||||
/**
|
||||
* 数据权限过滤器接口
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.datapermission;
|
||||
package top.continew.starter.data.mp.datapermission;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.datapermission;
|
||||
package top.continew.starter.data.mp.datapermission;
|
||||
|
||||
/**
|
||||
* 数据权限枚举
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.enums;
|
||||
package top.continew.starter.data.mp.enums;
|
||||
|
||||
/**
|
||||
* MyBatis ID 生成器类型枚举
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.handler;
|
||||
package top.continew.starter.data.mp.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.service;
|
||||
package top.continew.starter.data.mp.service;
|
||||
|
||||
/**
|
||||
* 通用业务接口
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.service.impl;
|
||||
package top.continew.starter.data.mp.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import top.continew.starter.core.util.ReflectUtils;
|
||||
import top.continew.starter.core.util.validate.CheckUtils;
|
||||
import top.continew.starter.data.mybatis.plus.base.BaseMapper;
|
||||
import top.continew.starter.data.mybatis.plus.service.IService;
|
||||
import top.continew.starter.data.mp.base.BaseMapper;
|
||||
import top.continew.starter.data.mp.service.IService;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Field;
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.data.mybatis.plus.query;
|
||||
package top.continew.starter.data.mp.util;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -23,18 +23,17 @@ import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import top.continew.starter.core.exception.BadRequestException;
|
||||
import top.continew.starter.core.util.ReflectUtils;
|
||||
import top.continew.starter.core.util.validate.ValidationUtils;
|
||||
import top.continew.starter.data.core.annotation.Query;
|
||||
import top.continew.starter.data.core.annotation.QueryIgnore;
|
||||
import top.continew.starter.data.core.enums.QueryType;
|
||||
import top.continew.starter.data.core.util.SqlInjectionUtils;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
@@ -60,11 +59,33 @@ public class QueryWrapperHelper {
|
||||
* @return QueryWrapper
|
||||
*/
|
||||
public static <Q, R> QueryWrapper<R> build(Q query) {
|
||||
return build(query, Sort.unsorted());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 QueryWrapper
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @param sort 排序条件
|
||||
* @param <Q> 查询条件数据类型
|
||||
* @param <R> 查询数据类型
|
||||
* @return QueryWrapper
|
||||
* @since 2.5.2
|
||||
*/
|
||||
public static <Q, R> QueryWrapper<R> build(Q query, Sort sort) {
|
||||
QueryWrapper<R> queryWrapper = new QueryWrapper<>();
|
||||
// 没有查询条件,直接返回
|
||||
if (null == query) {
|
||||
return queryWrapper;
|
||||
}
|
||||
// 设置排序条件
|
||||
if (sort != null && sort.isSorted()) {
|
||||
for (Sort.Order order : sort) {
|
||||
String field = CharSequenceUtil.toUnderlineCase(order.getProperty());
|
||||
ValidationUtils.throwIf(SqlInjectionUtils.check(field), "排序字段包含非法字符");
|
||||
queryWrapper.orderBy(true, order.isAscending(), field);
|
||||
}
|
||||
}
|
||||
// 获取查询条件中所有的字段
|
||||
List<Field> fieldList = ReflectUtils.getNonStaticFields(query.getClass());
|
||||
return build(query, fieldList, queryWrapper);
|
||||
@@ -0,0 +1 @@
|
||||
top.continew.starter.data.mp.autoconfigure.MybatisPlusAutoConfiguration
|
||||
@@ -1 +0,0 @@
|
||||
top.continew.starter.data.mybatis.flex.autoconfigure.MybatisFlexAutoConfiguration
|
||||
@@ -1 +0,0 @@
|
||||
top.continew.starter.data.mybatis.plus.autoconfigure.MybatisPlusAutoConfiguration
|
||||
@@ -15,8 +15,8 @@
|
||||
|
||||
<modules>
|
||||
<module>continew-starter-data-core</module>
|
||||
<module>continew-starter-data-mybatis-plus</module>
|
||||
<module>continew-starter-data-mybatis-flex</module>
|
||||
<module>continew-starter-data-mp</module>
|
||||
<module>continew-starter-data-mf</module>
|
||||
</modules>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -43,33 +43,33 @@
|
||||
|
||||
<properties>
|
||||
<!-- 项目版本号 -->
|
||||
<revision>2.5.0</revision>
|
||||
<snail-job.version>1.1.0</snail-job.version>
|
||||
<sa-token.version>1.38.0</sa-token.version>
|
||||
<revision>2.6.0</revision>
|
||||
<snail-job.version>1.1.2</snail-job.version>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<just-auth.version>1.16.6</just-auth.version>
|
||||
<mybatis-plus.version>3.5.7</mybatis-plus.version>
|
||||
<mybatis-flex.version>1.9.3</mybatis-flex.version>
|
||||
<mybatis-flex.version>1.9.7</mybatis-flex.version>
|
||||
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
|
||||
<p6spy.version>3.9.1</p6spy.version>
|
||||
<jetcache.version>2.7.6</jetcache.version>
|
||||
<redisson.version>3.32.0</redisson.version>
|
||||
<cosid.version>2.9.1</cosid.version>
|
||||
<redisson.version>3.35.0</redisson.version>
|
||||
<cosid.version>2.9.6</cosid.version>
|
||||
<sms4j.version>3.2.1</sms4j.version>
|
||||
<aj-captcha.version>1.3.0</aj-captcha.version>
|
||||
<easy-captcha.version>1.6.2</easy-captcha.version>
|
||||
<easy-excel.version>3.3.4</easy-excel.version>
|
||||
<nashorn.version>15.4</nashorn.version>
|
||||
<x-file-storage.version>2.2.0</x-file-storage.version>
|
||||
<aws-s3.version>1.12.761</aws-s3.version>
|
||||
<graceful-response.version>4.0.1-boot3</graceful-response.version>
|
||||
<aws-s3.version>1.12.771</aws-s3.version>
|
||||
<graceful-response.version>5.0.0-boot3</graceful-response.version>
|
||||
<crane4j.version>2.9.0</crane4j.version>
|
||||
<knife4j.version>4.5.0</knife4j.version>
|
||||
<tlog.version>1.5.2</tlog.version>
|
||||
<snakeyaml.version>2.2</snakeyaml.version>
|
||||
<snakeyaml.version>2.3</snakeyaml.version>
|
||||
<okhttp.version>4.12.0</okhttp.version>
|
||||
<ttl.version>2.14.5</ttl.version>
|
||||
<ip2region.version>3.2.6</ip2region.version>
|
||||
<hutool.version>5.8.29</hutool.version>
|
||||
<hutool.version>5.8.32</hutool.version>
|
||||
<!-- Maven Plugin Versions -->
|
||||
<flatten.version>1.6.0</flatten.version>
|
||||
<spotless.version>2.43.0</spotless.version>
|
||||
@@ -137,6 +137,11 @@
|
||||
<artifactId>mybatis-plus-core</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-extension</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Flex(MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率) -->
|
||||
<dependency>
|
||||
@@ -373,14 +378,14 @@
|
||||
<!-- 数据访问模块 - MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-data-mybatis-plus</artifactId>
|
||||
<artifactId>continew-starter-data-mp</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 数据访问模块 - MyBatis Flex -->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-data-mybatis-flex</artifactId>
|
||||
<artifactId>continew-starter-data-mf</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
<artifactId>spring-data-commons</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 模块 -->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 认证模块 - SaToken -->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
@@ -36,16 +42,16 @@
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 数据访问模块 - 核心模块 -->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-data-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 文件处理模块 - Excel -->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-file-excel</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 模块 -->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-web</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -21,6 +21,8 @@ import cn.hutool.core.util.ArrayUtil;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.util.validate.ValidationUtils;
|
||||
import top.continew.starter.data.core.util.SqlInjectionUtils;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
@@ -54,20 +56,17 @@ public class SortQuery implements Serializable {
|
||||
if (ArrayUtil.isEmpty(sort)) {
|
||||
return Sort.unsorted();
|
||||
}
|
||||
|
||||
ValidationUtils.throwIf(sort.length < 2, "排序条件非法");
|
||||
List<Sort.Order> orders = new ArrayList<>(sort.length);
|
||||
if (CharSequenceUtil.contains(sort[0], StringConstants.COMMA)) {
|
||||
// e.g "sort=createTime,desc&sort=name,asc"
|
||||
for (String s : sort) {
|
||||
List<String> sortList = CharSequenceUtil.splitTrim(s, StringConstants.COMMA);
|
||||
Sort.Order order = new Sort.Order(Sort.Direction.valueOf(sortList.get(1).toUpperCase()), sortList
|
||||
.get(0));
|
||||
orders.add(order);
|
||||
orders.add(this.getOrder(sortList.get(0), sortList.get(1)));
|
||||
}
|
||||
} else {
|
||||
// e.g "sort=createTime,desc"
|
||||
Sort.Order order = new Sort.Order(Sort.Direction.valueOf(sort[1].toUpperCase()), sort[0]);
|
||||
orders.add(order);
|
||||
orders.add(this.getOrder(sort[0], sort[1]));
|
||||
}
|
||||
return Sort.by(orders);
|
||||
}
|
||||
@@ -75,4 +74,16 @@ public class SortQuery implements Serializable {
|
||||
public void setSort(String[] sort) {
|
||||
this.sort = sort;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取排序条件
|
||||
*
|
||||
* @param field 字段
|
||||
* @param direction 排序方向
|
||||
* @return 排序条件
|
||||
*/
|
||||
private Sort.Order getOrder(String field, String direction) {
|
||||
ValidationUtils.throwIf(SqlInjectionUtils.check(field), "排序字段包含非法字符");
|
||||
return new Sort.Order(Sort.Direction.valueOf(direction.toUpperCase()), field);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.crud.model.resp;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页信息
|
||||
*
|
||||
* @param <T> 列表数据类型
|
||||
* @author Charles7c
|
||||
* @since 2.5.2
|
||||
*/
|
||||
@Schema(description = "分页信息")
|
||||
public class BasePageResp<T> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 列表数据
|
||||
*/
|
||||
@Schema(description = "列表数据")
|
||||
private List<T> list;
|
||||
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
@Schema(description = "总记录数", example = "10")
|
||||
private long total;
|
||||
|
||||
public BasePageResp() {
|
||||
}
|
||||
|
||||
public BasePageResp(final List<T> list, final long total) {
|
||||
this.list = list;
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public List<T> getList() {
|
||||
return list;
|
||||
}
|
||||
|
||||
public void setList(List<T> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
public long getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(long total) {
|
||||
this.total = total;
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
<!-- 数据访问模块 - MyBatis Flex -->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-data-mybatis-flex</artifactId>
|
||||
<artifactId>continew-starter-data-mf</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -22,7 +22,6 @@ import com.mybatisflex.core.paginate.Page;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -35,22 +34,17 @@ import java.util.List;
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "分页信息")
|
||||
public class PageResp<L> implements Serializable {
|
||||
public class PageResp<L> extends BasePageResp<L> {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 列表数据
|
||||
*/
|
||||
@Schema(description = "列表数据")
|
||||
private List<L> list;
|
||||
public PageResp() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
@Schema(description = "总记录数", example = "10")
|
||||
private long total;
|
||||
public PageResp(final List<L> list, final long total) {
|
||||
super(list, total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 MyBatis Plus 分页数据构建分页信息,并将源数据转换为指定类型数据
|
||||
@@ -65,10 +59,7 @@ public class PageResp<L> implements Serializable {
|
||||
if (null == page) {
|
||||
return empty();
|
||||
}
|
||||
PageResp<L> pageResp = new PageResp<>();
|
||||
pageResp.setList(BeanUtil.copyToList(page.getRecords(), targetClass));
|
||||
pageResp.setTotal(page.getTotalRow());
|
||||
return pageResp;
|
||||
return new PageResp<>(BeanUtil.copyToList(page.getRecords(), targetClass), page.getTotalRow());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,10 +73,7 @@ public class PageResp<L> implements Serializable {
|
||||
if (null == page) {
|
||||
return empty();
|
||||
}
|
||||
PageResp<L> pageResp = new PageResp<>();
|
||||
pageResp.setList(page.getRecords());
|
||||
pageResp.setTotal(page.getTotalRow());
|
||||
return pageResp;
|
||||
return new PageResp<>(page.getRecords(), page.getTotalRow());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,24 +111,6 @@ public class PageResp<L> implements Serializable {
|
||||
* @return 分页信息
|
||||
*/
|
||||
private static <L> PageResp<L> empty() {
|
||||
PageResp<L> pageResp = new PageResp<>();
|
||||
pageResp.setList(Collections.emptyList());
|
||||
return pageResp;
|
||||
}
|
||||
|
||||
public List<L> getList() {
|
||||
return list;
|
||||
}
|
||||
|
||||
public void setList(List<L> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
public long getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(long total) {
|
||||
this.total = total;
|
||||
return new PageResp<>(Collections.emptyList(), 0L);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ 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.lang.Opt;
|
||||
import cn.hutool.core.lang.tree.Tree;
|
||||
import cn.hutool.core.lang.tree.TreeNodeConfig;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
@@ -34,9 +33,9 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.util.ReflectUtils;
|
||||
import top.continew.starter.core.util.validate.ValidationUtils;
|
||||
import top.continew.starter.data.mybatis.flex.base.BaseMapper;
|
||||
import top.continew.starter.data.mybatis.flex.query.QueryWrapperHelper;
|
||||
import top.continew.starter.data.mybatis.flex.service.impl.ServiceImpl;
|
||||
import top.continew.starter.data.mf.base.BaseMapper;
|
||||
import top.continew.starter.data.mf.util.QueryWrapperHelper;
|
||||
import top.continew.starter.data.mf.service.impl.ServiceImpl;
|
||||
import top.continew.starter.extension.crud.annotation.TreeField;
|
||||
import top.continew.starter.extension.crud.model.query.PageQuery;
|
||||
import top.continew.starter.extension.crud.model.query.SortQuery;
|
||||
@@ -73,6 +72,7 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
|
||||
@Override
|
||||
public PageResp<L> page(Q query, PageQuery pageQuery) {
|
||||
QueryWrapper queryWrapper = this.buildQueryWrapper(query);
|
||||
this.sort(queryWrapper, pageQuery);
|
||||
Page<T> page = mapper.paginate(pageQuery.getPage(), pageQuery.getSize(), queryWrapper);
|
||||
PageResp<L> pageResp = PageResp.build(page, listClass);
|
||||
pageResp.getList().forEach(this::fill);
|
||||
@@ -185,12 +185,12 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
|
||||
* @param sortQuery 排序查询条件
|
||||
*/
|
||||
protected void sort(QueryWrapper queryWrapper, SortQuery sortQuery) {
|
||||
Sort sort = Opt.ofNullable(sortQuery).orElseGet(SortQuery::new).getSort();
|
||||
if (sortQuery == null || sortQuery.getSort().isUnsorted()) {
|
||||
return;
|
||||
}
|
||||
Sort sort = sortQuery.getSort();
|
||||
List<Field> entityFields = ReflectUtils.getNonStaticFields(this.entityClass);
|
||||
for (Sort.Order order : sort) {
|
||||
if (null == order) {
|
||||
continue;
|
||||
}
|
||||
String property = order.getProperty();
|
||||
String checkProperty;
|
||||
// 携带表别名则获取 . 后面的字段名
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<!-- 数据访问模块 - MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-data-mybatis-plus</artifactId>
|
||||
<artifactId>continew-starter-data-mp</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,102 +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.crud.model.query;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.hibernate.validator.constraints.Range;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 分页查询条件
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@ParameterObject
|
||||
@Schema(description = "分页查询条件")
|
||||
public class PageQuery extends SortQuery {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 默认页码:1
|
||||
*/
|
||||
private static final int DEFAULT_PAGE = 1;
|
||||
/**
|
||||
* 默认每页条数:10
|
||||
*/
|
||||
private static final int DEFAULT_SIZE = 10;
|
||||
|
||||
/**
|
||||
* 页码
|
||||
*/
|
||||
@Schema(description = "页码", example = "1")
|
||||
@Min(value = 1, message = "页码最小值为 {value}")
|
||||
private Integer page = DEFAULT_PAGE;
|
||||
|
||||
/**
|
||||
* 每页条数
|
||||
*/
|
||||
@Schema(description = "每页条数", example = "10")
|
||||
@Range(min = 1, max = 1000, message = "每页条数(取值范围 {min}-{max})")
|
||||
private Integer size = DEFAULT_SIZE;
|
||||
|
||||
/**
|
||||
* 基于分页查询条件转换为 MyBatis Plus 分页条件
|
||||
*
|
||||
* @param <T> 列表数据类型
|
||||
* @return MyBatis Plus 分页条件
|
||||
*/
|
||||
public <T> IPage<T> toPage() {
|
||||
Page<T> mybatisPage = new Page<>(this.getPage(), this.getSize());
|
||||
Sort pageSort = this.getSort();
|
||||
if (CollUtil.isNotEmpty(pageSort)) {
|
||||
for (Sort.Order order : pageSort) {
|
||||
OrderItem orderItem = new OrderItem();
|
||||
orderItem.setAsc(order.isAscending());
|
||||
orderItem.setColumn(CharSequenceUtil.toUnderlineCase(order.getProperty()));
|
||||
mybatisPage.addOrder(orderItem);
|
||||
}
|
||||
}
|
||||
return mybatisPage;
|
||||
}
|
||||
|
||||
public Integer getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public void setPage(Integer page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
public Integer getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(Integer size) {
|
||||
this.size = size;
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -34,22 +34,17 @@ import java.util.List;
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "分页信息")
|
||||
public class PageResp<L> implements Serializable {
|
||||
public class PageResp<L> extends BasePageResp<L> {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 列表数据
|
||||
*/
|
||||
@Schema(description = "列表数据")
|
||||
private List<L> list;
|
||||
public PageResp() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
@Schema(description = "总记录数", example = "10")
|
||||
private long total;
|
||||
public PageResp(final List<L> list, final long total) {
|
||||
super(list, total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 MyBatis Plus 分页数据构建分页信息,并将源数据转换为指定类型数据
|
||||
@@ -64,10 +59,7 @@ public class PageResp<L> implements Serializable {
|
||||
if (null == page) {
|
||||
return empty();
|
||||
}
|
||||
PageResp<L> pageResp = new PageResp<>();
|
||||
pageResp.setList(BeanUtil.copyToList(page.getRecords(), targetClass));
|
||||
pageResp.setTotal(page.getTotal());
|
||||
return pageResp;
|
||||
return new PageResp<>(BeanUtil.copyToList(page.getRecords(), targetClass), page.getTotal());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,10 +73,7 @@ public class PageResp<L> implements Serializable {
|
||||
if (null == page) {
|
||||
return empty();
|
||||
}
|
||||
PageResp<L> pageResp = new PageResp<>();
|
||||
pageResp.setList(page.getRecords());
|
||||
pageResp.setTotal(page.getTotal());
|
||||
return pageResp;
|
||||
return new PageResp<>(page.getRecords(), page.getTotal());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,24 +111,6 @@ public class PageResp<L> implements Serializable {
|
||||
* @return 分页信息
|
||||
*/
|
||||
private static <L> PageResp<L> empty() {
|
||||
PageResp<L> pageResp = new PageResp<>();
|
||||
pageResp.setList(new ArrayList<>(0));
|
||||
return pageResp;
|
||||
}
|
||||
|
||||
public List<L> getList() {
|
||||
return list;
|
||||
}
|
||||
|
||||
public void setList(List<L> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
public long getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(long total) {
|
||||
this.total = total;
|
||||
return new PageResp<>(Collections.emptyList(), 0L);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,15 +20,15 @@ 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.lang.Opt;
|
||||
import cn.hutool.core.lang.tree.Tree;
|
||||
import cn.hutool.core.lang.tree.TreeNodeConfig;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -37,22 +37,25 @@ import top.continew.starter.core.util.ClassUtils;
|
||||
import top.continew.starter.core.util.ReflectUtils;
|
||||
import top.continew.starter.core.util.validate.CheckUtils;
|
||||
import top.continew.starter.core.util.validate.ValidationUtils;
|
||||
import top.continew.starter.data.mybatis.plus.base.BaseMapper;
|
||||
import top.continew.starter.data.mybatis.plus.query.QueryWrapperHelper;
|
||||
import top.continew.starter.data.mybatis.plus.service.impl.ServiceImpl;
|
||||
import top.continew.starter.data.mp.base.BaseMapper;
|
||||
import top.continew.starter.data.mp.service.impl.ServiceImpl;
|
||||
import top.continew.starter.data.mp.util.QueryWrapperHelper;
|
||||
import top.continew.starter.extension.crud.annotation.DictField;
|
||||
import top.continew.starter.extension.crud.annotation.TreeField;
|
||||
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.extension.crud.service.BaseService;
|
||||
import top.continew.starter.extension.crud.util.TreeUtils;
|
||||
import top.continew.starter.extension.crud.model.query.PageQuery;
|
||||
import top.continew.starter.file.excel.util.ExcelUtils;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 业务实现基类
|
||||
@@ -76,7 +79,8 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
|
||||
@Override
|
||||
public PageResp<L> page(Q query, PageQuery pageQuery) {
|
||||
QueryWrapper<T> queryWrapper = this.buildQueryWrapper(query);
|
||||
IPage<T> page = baseMapper.selectPage(pageQuery.toPage(), queryWrapper);
|
||||
this.sort(queryWrapper, pageQuery);
|
||||
IPage<T> page = baseMapper.selectPage(new Page<>(pageQuery.getPage(), pageQuery.getSize()), queryWrapper);
|
||||
PageResp<L> pageResp = PageResp.build(page, this.getListClass());
|
||||
pageResp.getList().forEach(this::fill);
|
||||
return pageResp;
|
||||
@@ -253,9 +257,11 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
|
||||
* @param sortQuery 排序查询条件
|
||||
*/
|
||||
protected void sort(QueryWrapper<T> queryWrapper, SortQuery sortQuery) {
|
||||
Sort sort = Opt.ofNullable(sortQuery).orElseGet(SortQuery::new).getSort();
|
||||
if (sortQuery == null || sortQuery.getSort().isUnsorted()) {
|
||||
return;
|
||||
}
|
||||
Sort sort = sortQuery.getSort();
|
||||
for (Sort.Order order : sort) {
|
||||
if (null != order) {
|
||||
String property = order.getProperty();
|
||||
String checkProperty;
|
||||
// 携带表别名则获取 . 后面的字段名
|
||||
@@ -271,7 +277,6 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
|
||||
queryWrapper.orderBy(true, order.isAscending(), CharSequenceUtil.toUnderlineCase(property));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充数据
|
||||
|
||||
@@ -18,7 +18,6 @@ package top.continew.starter.log.core.model;
|
||||
|
||||
import top.continew.starter.log.core.enums.Include;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
@@ -78,7 +77,7 @@ public class LogRecord {
|
||||
* @return 日志记录器
|
||||
*/
|
||||
public static Started start(RecordableHttpRequest request) {
|
||||
return start(Clock.systemUTC(), request);
|
||||
return start(Instant.now(), request);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +87,7 @@ public class LogRecord {
|
||||
* @param request 请求信息
|
||||
* @return 日志记录器
|
||||
*/
|
||||
public static Started start(Clock timestamp, RecordableHttpRequest request) {
|
||||
public static Started start(Instant timestamp, RecordableHttpRequest request) {
|
||||
return new Started(timestamp, request);
|
||||
}
|
||||
|
||||
@@ -101,22 +100,11 @@ public class LogRecord {
|
||||
|
||||
private final RecordableHttpRequest request;
|
||||
|
||||
private Started(Clock clock, RecordableHttpRequest request) {
|
||||
this.timestamp = Instant.now(clock);
|
||||
private Started(Instant timestamp, RecordableHttpRequest request) {
|
||||
this.timestamp = timestamp;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束日志记录
|
||||
*
|
||||
* @param response 响应信息
|
||||
* @param includes 包含信息
|
||||
* @return 日志记录
|
||||
*/
|
||||
public LogRecord finish(RecordableHttpResponse response, Set<Include> includes) {
|
||||
return finish(Clock.systemUTC(), response, includes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束日志记录
|
||||
*
|
||||
@@ -125,10 +113,10 @@ public class LogRecord {
|
||||
* @param includes 包含信息
|
||||
* @return 日志记录
|
||||
*/
|
||||
public LogRecord finish(Clock clock, RecordableHttpResponse response, Set<Include> includes) {
|
||||
public LogRecord finish(Instant timestamp, RecordableHttpResponse response, Set<Include> includes) {
|
||||
LogRequest logRequest = new LogRequest(this.request, includes);
|
||||
LogResponse logResponse = new LogResponse(response, includes);
|
||||
Duration duration = Duration.between(this.timestamp, Instant.now(clock));
|
||||
Duration duration = Duration.between(this.timestamp, timestamp);
|
||||
return new LogRecord(this.timestamp, logRequest, logResponse, duration);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ package top.continew.starter.log.interceptor.autoconfigure;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.continew.starter.log.core.enums.Include;
|
||||
import top.continew.starter.web.util.SpringWebUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -41,13 +41,16 @@ public class LogProperties {
|
||||
|
||||
/**
|
||||
* 是否打印日志,开启后可打印访问日志(类似于 Nginx access log)
|
||||
* <p>
|
||||
* 不记录日志也支持开启打印访问日志
|
||||
* </p>
|
||||
*/
|
||||
private Boolean isPrint = false;
|
||||
|
||||
/**
|
||||
* 包含信息
|
||||
*/
|
||||
private Set<Include> includes = new HashSet<>(Include.defaultIncludes());
|
||||
private Set<Include> includes = Include.defaultIncludes();
|
||||
|
||||
/**
|
||||
* 放行路由
|
||||
@@ -85,4 +88,14 @@ public class LogProperties {
|
||||
public void setExcludePatterns(List<String> excludePatterns) {
|
||||
this.excludePatterns = excludePatterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否匹配放行路由
|
||||
*
|
||||
* @param uri 请求 URI
|
||||
* @return 是否匹配
|
||||
*/
|
||||
public boolean isMatch(String uri) {
|
||||
return this.getExcludePatterns().stream().anyMatch(pattern -> SpringWebUtils.isMatch(pattern, uri));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
import top.continew.starter.log.core.enums.Include;
|
||||
import top.continew.starter.log.interceptor.autoconfigure.LogProperties;
|
||||
import top.continew.starter.web.util.SpringWebUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
@@ -70,12 +69,13 @@ public class LogFilter extends OncePerRequestFilter implements Ordered {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
boolean isMatch = logProperties.isMatch(request.getRequestURI());
|
||||
// 包装输入流,可重复读取
|
||||
if (this.isRequestWrapper(request)) {
|
||||
if (!isMatch && this.isRequestWrapper(request)) {
|
||||
request = new ContentCachingRequestWrapper(request);
|
||||
}
|
||||
// 包装输出流,可重复读取
|
||||
boolean isResponseWrapper = this.isResponseWrapper(response);
|
||||
boolean isResponseWrapper = !isMatch && this.isResponseWrapper(response);
|
||||
if (isResponseWrapper) {
|
||||
response = new ContentCachingResponseWrapper(response);
|
||||
}
|
||||
@@ -98,14 +98,7 @@ public class LogFilter extends OncePerRequestFilter implements Ordered {
|
||||
}
|
||||
// 不拦截 /error
|
||||
ServerProperties serverProperties = SpringUtil.getBean(ServerProperties.class);
|
||||
if (request.getRequestURI().equals(serverProperties.getError().getPath())) {
|
||||
return false;
|
||||
}
|
||||
// 放行
|
||||
boolean isMatch = logProperties.getExcludePatterns()
|
||||
.stream()
|
||||
.anyMatch(pattern -> SpringWebUtils.match(pattern, request.getRequestURI()));
|
||||
return !isMatch;
|
||||
return !request.getRequestURI().equals(serverProperties.getError().getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,10 +32,11 @@ import top.continew.starter.log.core.annotation.Log;
|
||||
import top.continew.starter.log.core.dao.LogDao;
|
||||
import top.continew.starter.log.core.enums.Include;
|
||||
import top.continew.starter.log.core.model.LogRecord;
|
||||
import top.continew.starter.log.core.model.LogResponse;
|
||||
import top.continew.starter.log.interceptor.autoconfigure.LogProperties;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -49,7 +50,8 @@ public class LogInterceptor implements HandlerInterceptor {
|
||||
private static final Logger log = LoggerFactory.getLogger(LogInterceptor.class);
|
||||
private final LogDao logDao;
|
||||
private final LogProperties logProperties;
|
||||
private final TransmittableThreadLocal<LogRecord.Started> timestampTtl = new TransmittableThreadLocal<>();
|
||||
private final TransmittableThreadLocal<Instant> timeTtl = new TransmittableThreadLocal<>();
|
||||
private final TransmittableThreadLocal<LogRecord.Started> logTtl = new TransmittableThreadLocal<>();
|
||||
|
||||
public LogInterceptor(LogDao logDao, LogProperties logProperties) {
|
||||
this.logDao = logDao;
|
||||
@@ -60,13 +62,14 @@ public class LogInterceptor implements HandlerInterceptor {
|
||||
public boolean preHandle(@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull Object handler) {
|
||||
Clock timestamp = Clock.systemUTC();
|
||||
if (this.isRequestRecord(handler)) {
|
||||
Instant startTime = Instant.now();
|
||||
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
|
||||
log.info("[{}] {}", request.getMethod(), request.getRequestURI());
|
||||
timeTtl.set(startTime);
|
||||
}
|
||||
LogRecord.Started startedLogRecord = LogRecord.start(timestamp, new RecordableServletHttpRequest(request));
|
||||
timestampTtl.set(startedLogRecord);
|
||||
if (this.isRequestRecord(handler, request)) {
|
||||
LogRecord.Started startedLogRecord = LogRecord.start(startTime, new RecordableServletHttpRequest(request));
|
||||
logTtl.set(startedLogRecord);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -76,18 +79,23 @@ public class LogInterceptor implements HandlerInterceptor {
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull Object handler,
|
||||
Exception e) {
|
||||
LogRecord.Started startedLogRecord = timestampTtl.get();
|
||||
try {
|
||||
Instant endTime = Instant.now();
|
||||
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
|
||||
Duration timeTaken = Duration.between(timeTtl.get(), endTime);
|
||||
log.info("[{}] {} {} {}ms", request.getMethod(), request.getRequestURI(), response
|
||||
.getStatus(), timeTaken.toMillis());
|
||||
}
|
||||
LogRecord.Started startedLogRecord = logTtl.get();
|
||||
if (null == startedLogRecord) {
|
||||
return;
|
||||
}
|
||||
timestampTtl.remove();
|
||||
try {
|
||||
HandlerMethod handlerMethod = (HandlerMethod)handler;
|
||||
Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
|
||||
Log classLog = handlerMethod.getBeanType().getDeclaredAnnotation(Log.class);
|
||||
Set<Include> includeSet = this.getIncludes(methodLog, classLog);
|
||||
LogRecord finishedLogRecord = startedLogRecord.finish(new RecordableServletHttpResponse(response, response
|
||||
.getStatus()), includeSet);
|
||||
LogRecord finishedLogRecord = startedLogRecord
|
||||
.finish(endTime, new RecordableServletHttpResponse(response, response.getStatus()), includeSet);
|
||||
// 记录日志描述
|
||||
if (includeSet.contains(Include.DESCRIPTION)) {
|
||||
this.logDescription(finishedLogRecord, methodLog, handlerMethod);
|
||||
@@ -96,14 +104,12 @@ public class LogInterceptor implements HandlerInterceptor {
|
||||
if (includeSet.contains(Include.MODULE)) {
|
||||
this.logModule(finishedLogRecord, methodLog, classLog, handlerMethod);
|
||||
}
|
||||
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
|
||||
LogResponse logResponse = finishedLogRecord.getResponse();
|
||||
log.info("[{}] {} {} {}ms", request.getMethod(), request.getRequestURI(), logResponse
|
||||
.getStatus(), finishedLogRecord.getTimeTaken().toMillis());
|
||||
}
|
||||
logDao.add(finishedLogRecord);
|
||||
} catch (Exception ex) {
|
||||
log.error("Logging http log occurred an error: {}.", ex.getMessage(), ex);
|
||||
} finally {
|
||||
timeTtl.remove();
|
||||
logTtl.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +121,7 @@ public class LogInterceptor implements HandlerInterceptor {
|
||||
* @return 日志包含信息
|
||||
*/
|
||||
private Set<Include> getIncludes(Log methodLog, Log classLog) {
|
||||
Set<Include> includeSet = logProperties.getIncludes();
|
||||
Set<Include> includeSet = new HashSet<>(logProperties.getIncludes());
|
||||
if (null != classLog) {
|
||||
this.processInclude(includeSet, classLog);
|
||||
}
|
||||
@@ -194,10 +200,14 @@ public class LogInterceptor implements HandlerInterceptor {
|
||||
* @param handler 处理器
|
||||
* @return true:需要记录;false:不需要记录
|
||||
*/
|
||||
private boolean isRequestRecord(Object handler) {
|
||||
private boolean isRequestRecord(Object handler, HttpServletRequest request) {
|
||||
if (!(handler instanceof HandlerMethod handlerMethod)) {
|
||||
return false;
|
||||
}
|
||||
// 如果接口匹配排除列表,不记录日志
|
||||
if (logProperties.isMatch(request.getRequestURI())) {
|
||||
return false;
|
||||
}
|
||||
// 如果接口被隐藏,不记录日志
|
||||
Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
|
||||
if (null != methodOperation && methodOperation.hidden()) {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<!-- MyBatis Plus(MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率) -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-core</artifactId>
|
||||
<artifactId>mybatis-plus-extension</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -50,7 +50,7 @@ public class CryptoAutoConfiguration {
|
||||
* MyBatis 加密拦截器配置
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(MyBatisEncryptInterceptor.class)
|
||||
@ConditionalOnMissingBean
|
||||
public MyBatisEncryptInterceptor myBatisEncryptInterceptor() {
|
||||
return new MyBatisEncryptInterceptor(properties);
|
||||
}
|
||||
|
||||
@@ -16,15 +16,13 @@
|
||||
|
||||
package top.continew.starter.security.crypto.core;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.plugin.*;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.exception.BusinessException;
|
||||
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;
|
||||
@@ -42,30 +40,100 @@ import java.util.stream.Stream;
|
||||
* @author Charles7c
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public abstract class AbstractMyBatisInterceptor implements Interceptor {
|
||||
public abstract class AbstractMyBatisInterceptor {
|
||||
|
||||
private static final Map<Class<?>, List<Field>> CLASS_FIELD_CACHE = new ConcurrentHashMap<>();
|
||||
private static final Map<String, Map<String, FieldEncrypt>> ENCRYPT_PARAM_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 获取加密参数
|
||||
* 获取所有字符串类型、需要加/解密的、有值字段
|
||||
*
|
||||
* @param mappedStatementId 映射语句 ID
|
||||
* @return 加密参数
|
||||
* @param obj 对象
|
||||
* @return 字段列表
|
||||
*/
|
||||
public Map<String, FieldEncrypt> getEncryptParams(String mappedStatementId) {
|
||||
return getEncryptParams(mappedStatementId, null);
|
||||
protected List<Field> getEncryptFields(Object obj) {
|
||||
if (null == obj) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return this.getEncryptFields(obj.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有字符串类型、需要加/解密的、有值字段
|
||||
*
|
||||
* @param clazz 类型对象
|
||||
* @return 字段列表
|
||||
*/
|
||||
protected List<Field> getEncryptFields(Class<?> clazz) {
|
||||
return CLASS_FIELD_CACHE.computeIfAbsent(clazz, key -> Arrays.stream(ReflectUtil.getFields(clazz))
|
||||
.filter(field -> String.class.equals(field.getType()))
|
||||
.filter(field -> null != field.getAnnotation(FieldEncrypt.class))
|
||||
.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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密参数
|
||||
*
|
||||
* @param mappedStatementId 映射语句 ID
|
||||
* @param parameterIndex 参数索引
|
||||
* @return 加密参数
|
||||
* @param mappedStatement 映射语句
|
||||
* @return 获取加密参数
|
||||
*/
|
||||
public Map<String, FieldEncrypt> getEncryptParams(String mappedStatementId, Integer parameterIndex) {
|
||||
return ENCRYPT_PARAM_CACHE
|
||||
.computeIfAbsent(mappedStatementId, key -> getEncryptParamsNoCached(mappedStatementId, parameterIndex));
|
||||
protected Map<String, FieldEncrypt> getEncryptParameters(MappedStatement mappedStatement) {
|
||||
String mappedStatementId = mappedStatement.getId();
|
||||
return ENCRYPT_PARAM_CACHE.computeIfAbsent(mappedStatementId, key -> {
|
||||
Method method = this.getMethod(mappedStatementId);
|
||||
if (null == method) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, FieldEncrypt> encryptMap = new HashMap<>();
|
||||
Parameter[] parameters = method.getParameters();
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
Parameter parameter = parameters[i];
|
||||
FieldEncrypt fieldEncrypt = parameter.getAnnotation(FieldEncrypt.class);
|
||||
if (null == fieldEncrypt) {
|
||||
continue;
|
||||
}
|
||||
String parameterName = this.getParameterName(parameter);
|
||||
encryptMap.put(parameterName, fieldEncrypt);
|
||||
if (String.class.equals(parameter.getType())) {
|
||||
encryptMap.put("param" + (i + 1), fieldEncrypt);
|
||||
}
|
||||
}
|
||||
return encryptMap;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取映射方法
|
||||
*
|
||||
* @param mappedStatementId 映射语句 ID
|
||||
* @return 映射方法
|
||||
*/
|
||||
private Method getMethod(String mappedStatementId) {
|
||||
String className = CharSequenceUtil.subBefore(mappedStatementId, StringConstants.DOT, true);
|
||||
String methodName = CharSequenceUtil.subAfter(mappedStatementId, StringConstants.DOT, true);
|
||||
try {
|
||||
Method[] methods = ReflectUtil.getMethods(Class.forName(className));
|
||||
return Stream.of(methods).filter(method -> method.getName().equals(methodName)).findFirst().orElse(null);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new BaseException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,88 +146,4 @@ public abstract class AbstractMyBatisInterceptor implements Interceptor {
|
||||
Param param = parameter.getAnnotation(Param.class);
|
||||
return null != param ? param.value() : parameter.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有字符串类型、需要加/解密的、有值字段
|
||||
*
|
||||
* @param obj 对象
|
||||
* @return 字段列表
|
||||
*/
|
||||
public List<Field> getEncryptFields(Object obj) {
|
||||
if (null == obj) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Arrays.stream(ReflectUtil.getFields(obj.getClass()))
|
||||
.filter(field -> String.class.equals(field.getType()))
|
||||
.filter(field -> null != field.getAnnotation(FieldEncrypt.class))
|
||||
.filter(field -> null != ReflectUtil.getFieldValue(obj, field))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段加/解密处理器
|
||||
*
|
||||
* @param fieldEncrypt 字段加密注解
|
||||
* @return 加/解密处理器
|
||||
*/
|
||||
public 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数列表(无缓存)
|
||||
*
|
||||
* @param mappedStatementId 映射语句 ID
|
||||
* @param parameterIndex 参数数量
|
||||
* @return 参数列表
|
||||
*/
|
||||
private Map<String, FieldEncrypt> getEncryptParamsNoCached(String mappedStatementId, Integer parameterIndex) {
|
||||
try {
|
||||
String className = CharSequenceUtil.subBefore(mappedStatementId, StringConstants.DOT, true);
|
||||
String wrapperMethodName = CharSequenceUtil.subAfter(mappedStatementId, StringConstants.DOT, true);
|
||||
String methodName = Stream.of("_mpCount", "_COUNT")
|
||||
.filter(wrapperMethodName::endsWith)
|
||||
.findFirst()
|
||||
.map(suffix -> wrapperMethodName.substring(0, wrapperMethodName.length() - suffix.length()))
|
||||
.orElse(wrapperMethodName);
|
||||
// 获取真实方法
|
||||
Optional<Method> methodOptional = Arrays.stream(ReflectUtil.getMethods(Class.forName(className), m -> {
|
||||
if (Objects.nonNull(parameterIndex)) {
|
||||
return Objects.equals(m.getName(), methodName) && m.getParameterCount() == parameterIndex;
|
||||
}
|
||||
return Objects.equals(m.getName(), methodName);
|
||||
})).findFirst();
|
||||
if (methodOptional.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
// 获取方法中的加密参数
|
||||
Map<String, FieldEncrypt> map = MapUtil.newHashMap();
|
||||
Parameter[] parameterArr = methodOptional.get().getParameters();
|
||||
for (int i = 0; i < parameterArr.length; i++) {
|
||||
Parameter parameter = parameterArr[i];
|
||||
String parameterName = this.getParameterName(parameter);
|
||||
FieldEncrypt fieldEncrypt = parameter.getAnnotation(FieldEncrypt.class);
|
||||
if (null != fieldEncrypt) {
|
||||
map.put(parameterName, fieldEncrypt);
|
||||
if (String.class.equals(parameter.getType())) {
|
||||
map.put("param" + (i + 1), fieldEncrypt);
|
||||
}
|
||||
} else if (parameterName.startsWith(Constants.ENTITY)) {
|
||||
map.put(parameterName, null);
|
||||
} else if (parameterName.startsWith(Constants.WRAPPER)) {
|
||||
map.put(parameterName, null);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new BusinessException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ import java.util.List;
|
||||
* @since 1.4.0
|
||||
*/
|
||||
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
|
||||
public class MyBatisDecryptInterceptor extends AbstractMyBatisInterceptor {
|
||||
public class MyBatisDecryptInterceptor extends AbstractMyBatisInterceptor implements Interceptor {
|
||||
|
||||
private CryptoProperties properties;
|
||||
|
||||
@@ -65,6 +65,9 @@ public class MyBatisDecryptInterceptor extends AbstractMyBatisInterceptor {
|
||||
for (Field field : fieldList) {
|
||||
IEncryptor encryptor = super.getEncryptor(field.getAnnotation(FieldEncrypt.class));
|
||||
Object fieldValue = ReflectUtil.getFieldValue(result, field);
|
||||
if (null == fieldValue) {
|
||||
continue;
|
||||
}
|
||||
// 优先获取自定义对称加密算法密钥,获取不到时再获取全局配置
|
||||
String password = ObjectUtil.defaultIfBlank(field.getAnnotation(FieldEncrypt.class)
|
||||
.password(), properties.getPassword());
|
||||
|
||||
@@ -17,33 +17,27 @@
|
||||
package top.continew.starter.security.crypto.core;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import org.apache.ibatis.cache.CacheKey;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||
import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.mapping.SqlCommandType;
|
||||
import org.apache.ibatis.plugin.*;
|
||||
import org.apache.ibatis.session.ResultHandler;
|
||||
import org.apache.ibatis.session.RowBounds;
|
||||
import org.apache.ibatis.type.SimpleTypeRegistry;
|
||||
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.autoconfigure.CryptoProperties;
|
||||
import top.continew.starter.security.crypto.encryptor.IEncryptor;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 字段加密拦截器
|
||||
@@ -51,54 +45,43 @@ import java.util.*;
|
||||
* @author Charles7c
|
||||
* @since 1.4.0
|
||||
*/
|
||||
@Intercepts({@Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class}),
|
||||
@Signature(method = "query", type = Executor.class, args = {MappedStatement.class, Object.class, RowBounds.class,
|
||||
ResultHandler.class, CacheKey.class, BoundSql.class}),
|
||||
@Signature(method = "query", type = Executor.class, args = {MappedStatement.class, Object.class, RowBounds.class,
|
||||
ResultHandler.class})})
|
||||
public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor {
|
||||
public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor implements InnerInterceptor {
|
||||
|
||||
private CryptoProperties properties;
|
||||
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;
|
||||
}
|
||||
|
||||
public MyBatisEncryptInterceptor() {
|
||||
@Override
|
||||
public void beforeQuery(Executor executor,
|
||||
MappedStatement mappedStatement,
|
||||
Object parameterObject,
|
||||
RowBounds rowBounds,
|
||||
ResultHandler resultHandler,
|
||||
BoundSql boundSql) {
|
||||
if (null == parameterObject) {
|
||||
return;
|
||||
}
|
||||
if (parameterObject instanceof Map parameterMap) {
|
||||
this.encryptQueryParameter(parameterMap, mappedStatement);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object intercept(Invocation invocation) throws Throwable {
|
||||
Object[] args = invocation.getArgs();
|
||||
MappedStatement mappedStatement = (MappedStatement)args[0];
|
||||
Object parameter = args[1];
|
||||
if (!this.isEncryptRequired(parameter, mappedStatement.getSqlCommandType())) {
|
||||
return invocation.proceed();
|
||||
public void beforeUpdate(Executor executor, MappedStatement mappedStatement, Object parameterObject) {
|
||||
if (null == parameterObject) {
|
||||
return;
|
||||
}
|
||||
// 使用 @Param 注解的场景
|
||||
if (parameter instanceof HashMap parameterMap) {
|
||||
if (parameterObject instanceof Map parameterMap) {
|
||||
// 带别名方法(使用 @Param 注解的场景)
|
||||
this.encryptMap(parameterMap, mappedStatement);
|
||||
} else {
|
||||
this.doEncrypt(this.getEncryptFields(parameter), parameter);
|
||||
// 无别名方法(例如:MP insert 等方法)
|
||||
this.encryptEntity(super.getEncryptFields(parameterObject), parameterObject);
|
||||
}
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要加密处理
|
||||
*
|
||||
* @param parameter 参数
|
||||
* @param sqlCommandType SQL 类型
|
||||
* @return true:是;false:否
|
||||
*/
|
||||
private boolean isEncryptRequired(Object parameter, SqlCommandType sqlCommandType) {
|
||||
if (ObjectUtil.isEmpty(parameter)) {
|
||||
return false;
|
||||
}
|
||||
if (!(SqlCommandType.UPDATE == sqlCommandType || SqlCommandType.INSERT == sqlCommandType || SqlCommandType.SELECT == sqlCommandType)) {
|
||||
return false;
|
||||
}
|
||||
return !SimpleTypeRegistry.isSimpleType(parameter.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,141 +89,132 @@ public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor {
|
||||
*
|
||||
* @param parameterMap 参数
|
||||
* @param mappedStatement 映射语句
|
||||
* @throws Exception /
|
||||
*/
|
||||
private void encryptMap(HashMap<String, Object> parameterMap, MappedStatement mappedStatement) throws Exception {
|
||||
Map<String, FieldEncrypt> encryptParamMap = super.getEncryptParams(mappedStatement.getId(), parameterMap
|
||||
.isEmpty() ? null : parameterMap.size() / 2);
|
||||
for (Map.Entry<String, FieldEncrypt> encryptParamEntry : encryptParamMap.entrySet()) {
|
||||
String parameterName = encryptParamEntry.getKey();
|
||||
if (parameterName.startsWith(Constants.ENTITY)) {
|
||||
// 兼容 MyBatis Plus 封装的 update 相关方法,updateById、update
|
||||
Object entity = parameterMap.getOrDefault(parameterName, null);
|
||||
this.doEncrypt(this.getEncryptFields(entity), entity);
|
||||
} else if (parameterName.startsWith(Constants.WRAPPER)) {
|
||||
// 处理参数为 Wrapper 的情况
|
||||
Wrapper wrapper = (Wrapper)parameterMap.getOrDefault(parameterName, null);
|
||||
this.doEncrypt(wrapper, mappedStatement);
|
||||
private void encryptMap(Map<String, Object> parameterMap, MappedStatement mappedStatement) {
|
||||
Object parameter;
|
||||
// 别名带有 et(针对 MP 的 updateById、update 等方法)
|
||||
if (parameterMap.containsKey(Constants.ENTITY) && null != (parameter = parameterMap.get(Constants.ENTITY))) {
|
||||
this.encryptEntity(super.getEncryptFields(parameter), parameter);
|
||||
}
|
||||
// 别名带有 ew(针对 MP 的 UpdateWrapper、LambdaUpdateWrapper 等参数)
|
||||
if (parameterMap.containsKey(Constants.WRAPPER) && null != (parameter = parameterMap.get(Constants.WRAPPER))) {
|
||||
this.encryptUpdateWrapper(parameter, mappedStatement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密查询参数(针对 Map 类型参数)
|
||||
*
|
||||
* @param parameterMap 参数
|
||||
* @param mappedStatement 映射语句
|
||||
*/
|
||||
private void encryptQueryParameter(Map<String, Object> parameterMap, MappedStatement mappedStatement) {
|
||||
Map<String, FieldEncrypt> encryptParameterMap = super.getEncryptParameters(mappedStatement);
|
||||
for (Map.Entry<String, Object> parameterEntrySet : parameterMap.entrySet()) {
|
||||
String parameterName = parameterEntrySet.getKey();
|
||||
Object parameterValue = parameterEntrySet.getValue();
|
||||
if (null == parameterValue || ClassUtil.isBasicType(parameterValue
|
||||
.getClass()) || parameterValue instanceof AbstractWrapper) {
|
||||
continue;
|
||||
}
|
||||
if (parameterValue instanceof String str) {
|
||||
FieldEncrypt fieldEncrypt = encryptParameterMap.get(parameterName);
|
||||
if (null != fieldEncrypt) {
|
||||
parameterMap.put(parameterName, this.doEncrypt(str, fieldEncrypt));
|
||||
}
|
||||
} else {
|
||||
FieldEncrypt fieldEncrypt = encryptParamEntry.getValue();
|
||||
parameterMap.put(parameterName, this.doEncrypt(parameterMap.get(parameterName), fieldEncrypt));
|
||||
// 实体参数
|
||||
this.encryptEntity(super.getEncryptFields(parameterValue), parameterValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理加密
|
||||
* 处理 UpdateWrapper 类型参数加密(针对 MP 的 UpdateWrapper、LambdaUpdateWrapper 等参数)
|
||||
*
|
||||
* @param parameter Wrapper 参数
|
||||
* @param mappedStatement 映射语句
|
||||
* @since 2.1.1
|
||||
* @author cary
|
||||
* @author wangshaopeng@talkweb.com.cn(<a
|
||||
* href="https://blog.csdn.net/tianmaxingkonger/article/details/130986784">基于Mybatis-Plus拦截器实现MySQL数据加解密</a>)
|
||||
*/
|
||||
private void encryptUpdateWrapper(Object parameter, MappedStatement mappedStatement) {
|
||||
if (parameter instanceof AbstractWrapper updateWrapper) {
|
||||
String sqlSet = updateWrapper.getSqlSet();
|
||||
if (CharSequenceUtil.isBlank(sqlSet)) {
|
||||
return;
|
||||
}
|
||||
// 将 name=#{ew.paramNameValuePairs.xxx},age=#{ew.paramNameValuePairs.xxx} 切出来
|
||||
String[] elArr = sqlSet.split(StringConstants.COMMA);
|
||||
Map<String, String> propMap = new HashMap<>(elArr.length);
|
||||
Arrays.stream(elArr).forEach(el -> {
|
||||
String[] elPart = el.split(StringConstants.EQUALS);
|
||||
propMap.put(elPart[0], elPart[1]);
|
||||
});
|
||||
// 获取加密字段
|
||||
Class<?> entityClass = mappedStatement.getParameterMap().getType();
|
||||
List<Field> encryptFieldList = super.getEncryptFields(entityClass);
|
||||
for (Field field : encryptFieldList) {
|
||||
FieldEncrypt fieldEncrypt = field.getAnnotation(FieldEncrypt.class);
|
||||
String el = propMap.get(field.getName());
|
||||
if (CharSequenceUtil.isBlank(el)) {
|
||||
continue;
|
||||
}
|
||||
Matcher matcher = PARAM_PAIRS_PATTERN.matcher(el);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理实体加密
|
||||
*
|
||||
* @param fieldList 加密字段列表
|
||||
* @param entity 实体
|
||||
* @throws Exception /
|
||||
*/
|
||||
private void doEncrypt(List<Field> fieldList, Object entity) throws Exception {
|
||||
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 (null == fieldValue) {
|
||||
continue;
|
||||
}
|
||||
// 优先获取自定义对称加密算法密钥,获取不到时再获取全局配置
|
||||
String password = ObjectUtil.defaultIfBlank(field.getAnnotation(FieldEncrypt.class).password(), properties
|
||||
.getPassword());
|
||||
String ciphertext = encryptor.encrypt(fieldValue.toString(), password, properties.getPublicKey());
|
||||
String ciphertext;
|
||||
try {
|
||||
ciphertext = encryptor.encrypt(fieldValue.toString(), password, properties.getPublicKey());
|
||||
} catch (Exception e) {
|
||||
throw new BaseException(e);
|
||||
}
|
||||
ReflectUtil.setFieldValue(entity, field, ciphertext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Wrapper 加密
|
||||
*
|
||||
* @param wrapper Wrapper 对象
|
||||
* @param mappedStatement 映射语句
|
||||
* @throws Exception /
|
||||
*/
|
||||
private void doEncrypt(Wrapper wrapper, MappedStatement mappedStatement) throws Exception {
|
||||
if (wrapper instanceof AbstractWrapper abstractWrapper) {
|
||||
String sqlSet = abstractWrapper.getSqlSet();
|
||||
if (CharSequenceUtil.isEmpty(sqlSet)) {
|
||||
return;
|
||||
}
|
||||
String className = CharSequenceUtil.subBefore(mappedStatement.getId(), StringConstants.DOT, true);
|
||||
Class<?> mapperClass = Class.forName(className);
|
||||
Optional<Class> baseMapperGenerics = getEntityTypeByMapperClass(mapperClass, Optional.empty());
|
||||
// 获取不到泛型对象 则不进行下面的逻辑
|
||||
if (baseMapperGenerics.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(baseMapperGenerics.get());
|
||||
List<TableFieldInfo> fieldList = tableInfo.getFieldList();
|
||||
// 将 name=#{ew.paramNameValuePairs.xxx},age=#{ew.paramNameValuePairs.xxx} 切出来
|
||||
for (String sqlFragment : sqlSet.split(Constants.COMMA)) {
|
||||
String columnName = sqlFragment.split(Constants.EQUALS)[0];
|
||||
// 截取其中的 xxx 字符,例如:#{ew.paramNameValuePairs.xxx}
|
||||
String paramNameVal = sqlFragment.split(Constants.EQUALS)[1].substring(25, sqlFragment
|
||||
.split(Constants.EQUALS)[1].length() - 1);
|
||||
Optional<TableFieldInfo> fieldInfo = fieldList.stream()
|
||||
.filter(f -> f.getColumn().equals(columnName))
|
||||
.findAny();
|
||||
if (fieldInfo.isPresent()) {
|
||||
TableFieldInfo tableFieldInfo = fieldInfo.get();
|
||||
FieldEncrypt fieldEncrypt = tableFieldInfo.getField().getAnnotation(FieldEncrypt.class);
|
||||
if (fieldEncrypt != null) {
|
||||
Map<String, Object> paramNameValuePairs = abstractWrapper.getParamNameValuePairs();
|
||||
paramNameValuePairs.put(paramNameVal, this.doEncrypt(paramNameValuePairs
|
||||
.get(paramNameVal), fieldEncrypt));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理加密
|
||||
*
|
||||
* @param parameterValue 参数值
|
||||
* @param fieldEncrypt 字段加密注解
|
||||
* @throws Exception /
|
||||
*/
|
||||
private Object doEncrypt(Object parameterValue, FieldEncrypt fieldEncrypt) throws Exception {
|
||||
private Object doEncrypt(Object parameterValue, FieldEncrypt fieldEncrypt) {
|
||||
if (null == 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) {
|
||||
throw new BaseException(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Mapper 获取泛型
|
||||
*
|
||||
* @param mapperClass Mapper class
|
||||
* @param tempResult 临时存储的泛型对象
|
||||
* @return 泛型
|
||||
*/
|
||||
private static Optional<Class> getEntityTypeByMapperClass(Class<?> mapperClass, Optional<Class> tempResult) {
|
||||
Type[] genericInterfaces = mapperClass.getGenericInterfaces();
|
||||
Optional<Class> result = tempResult;
|
||||
for (Type genericInterface : genericInterfaces) {
|
||||
if (genericInterface instanceof ParameterizedType parameterizedType) {
|
||||
Type rawType = parameterizedType.getRawType();
|
||||
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
|
||||
// 如果匹配上 BaseMapper 且泛型参数是 Class 类型,则直接返回
|
||||
if (rawType.equals(BaseMapper.class)) {
|
||||
return actualTypeArguments[0] instanceof Class
|
||||
? Optional.of((Class)actualTypeArguments[0])
|
||||
: result;
|
||||
} else if (rawType instanceof Class interfaceClass) {
|
||||
// 如果泛型参数是 Class 类型,则传递给递归调用
|
||||
if (actualTypeArguments[0] instanceof Class tempResultClass) {
|
||||
result = Optional.of(tempResultClass);
|
||||
}
|
||||
// 递归调用,继续查找
|
||||
Optional<Class> innerResult = getEntityTypeByMapperClass(interfaceClass, result);
|
||||
if (innerResult.isPresent()) {
|
||||
return innerResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果没有找到,返回传递进来的 tempResult
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.web.autoconfigure.response;
|
||||
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import org.apache.commons.lang3.reflect.TypeUtils;
|
||||
import org.springdoc.core.parsers.ReturnTypeParser;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import top.continew.starter.apidoc.util.DocUtils;
|
||||
import top.continew.starter.web.model.R;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
/**
|
||||
* SpringDoc 全局响应处理器
|
||||
* <p>
|
||||
* 接口文档全局添加响应格式 {@link R}
|
||||
* </p>
|
||||
*
|
||||
* @author echo
|
||||
* @since 2.5.2
|
||||
*/
|
||||
public class ApiDocGlobalResponseHandler implements ReturnTypeParser {
|
||||
|
||||
private final GlobalResponseProperties globalResponseProperties;
|
||||
private final Class<Object> responseClass;
|
||||
|
||||
public ApiDocGlobalResponseHandler(GlobalResponseProperties globalResponseProperties) {
|
||||
this.globalResponseProperties = globalResponseProperties;
|
||||
this.responseClass = ClassUtil.loadClass(globalResponseProperties.getResponseClassFullName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取返回类型
|
||||
*
|
||||
* @param methodParameter 方法参数
|
||||
* @return {@link Type }
|
||||
*/
|
||||
@Override
|
||||
public Type getReturnType(MethodParameter methodParameter) {
|
||||
// 获取返回类型
|
||||
Type returnType = ReturnTypeParser.super.getReturnType(methodParameter);
|
||||
// 判断是否具有 RestController 注解
|
||||
if (!DocUtils.hasRestControllerAnnotation(methodParameter.getContainingClass())) {
|
||||
return returnType;
|
||||
}
|
||||
// 如果为响应类型,则直接返回
|
||||
if (returnType.getTypeName().contains(globalResponseProperties.getResponseClassFullName())) {
|
||||
return returnType;
|
||||
}
|
||||
// 如果是 void类型,则返回 R<Void>
|
||||
if (returnType == void.class || returnType == Void.class) {
|
||||
return TypeUtils.parameterize(responseClass, Void.class);
|
||||
}
|
||||
// 返回 R<T>
|
||||
return TypeUtils.parameterize(responseClass, returnType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.web.autoconfigure.response;
|
||||
|
||||
import com.feiniaojin.gracefulresponse.advice.lifecycle.exception.BeforeControllerAdviceProcess;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import top.continew.starter.web.util.SpringWebUtils;
|
||||
|
||||
/**
|
||||
* 默认回调处理器实现
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.6.0
|
||||
*/
|
||||
public class DefaultBeforeControllerAdviceProcessImpl implements BeforeControllerAdviceProcess {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(DefaultBeforeControllerAdviceProcessImpl.class);
|
||||
private final GlobalResponseProperties globalResponseProperties;
|
||||
|
||||
public DefaultBeforeControllerAdviceProcessImpl(GlobalResponseProperties globalResponseProperties) {
|
||||
this.globalResponseProperties = globalResponseProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void call(Throwable throwable) {
|
||||
if (globalResponseProperties.isPrintExceptionInGlobalAdvice()) {
|
||||
HttpServletRequest request = SpringWebUtils.getRequest();
|
||||
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,9 @@ package top.continew.starter.web.autoconfigure.response;
|
||||
|
||||
import com.feiniaojin.gracefulresponse.ExceptionAliasRegister;
|
||||
import com.feiniaojin.gracefulresponse.advice.*;
|
||||
import com.feiniaojin.gracefulresponse.advice.lifecycle.exception.BeforeControllerAdviceProcess;
|
||||
import com.feiniaojin.gracefulresponse.advice.lifecycle.exception.ControllerAdvicePredicate;
|
||||
import com.feiniaojin.gracefulresponse.advice.lifecycle.response.ResponseBodyAdvicePredicate;
|
||||
import com.feiniaojin.gracefulresponse.api.ResponseFactory;
|
||||
import com.feiniaojin.gracefulresponse.api.ResponseStatusFactory;
|
||||
import com.feiniaojin.gracefulresponse.defaults.DefaultResponseFactory;
|
||||
@@ -37,6 +40,7 @@ import top.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.continew.starter.core.util.GeneralPropertySourceFactory;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* 全局响应自动配置
|
||||
@@ -50,23 +54,10 @@ import java.util.Locale;
|
||||
public class GlobalResponseAutoConfiguration {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalResponseAutoConfiguration.class);
|
||||
private final GlobalResponseProperties globalResponseProperties;
|
||||
|
||||
/**
|
||||
* 全局异常处理
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GrGlobalExceptionAdvice globalExceptionAdvice() {
|
||||
return new GrGlobalExceptionAdvice();
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局校验异常处理
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GrValidationExceptionAdvice validationExceptionAdvice() {
|
||||
return new GrValidationExceptionAdvice();
|
||||
public GlobalResponseAutoConfiguration(GlobalResponseProperties globalResponseProperties) {
|
||||
this.globalResponseProperties = globalResponseProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,8 +65,13 @@ public class GlobalResponseAutoConfiguration {
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GrNotVoidResponseBodyAdvice notVoidResponseBodyAdvice() {
|
||||
return new GrNotVoidResponseBodyAdvice();
|
||||
public GrNotVoidResponseBodyAdvice grNotVoidResponseBodyAdvice() {
|
||||
GrNotVoidResponseBodyAdvice notVoidResponseBodyAdvice = new GrNotVoidResponseBodyAdvice();
|
||||
CopyOnWriteArrayList<ResponseBodyAdvicePredicate> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
|
||||
copyOnWriteArrayList.add(notVoidResponseBodyAdvice);
|
||||
notVoidResponseBodyAdvice.setPredicates(copyOnWriteArrayList);
|
||||
notVoidResponseBodyAdvice.setResponseBodyAdviceProcessor(notVoidResponseBodyAdvice);
|
||||
return notVoidResponseBodyAdvice;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,8 +79,104 @@ public class GlobalResponseAutoConfiguration {
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GrVoidResponseBodyAdvice voidResponseBodyAdvice() {
|
||||
return new GrVoidResponseBodyAdvice();
|
||||
public GrVoidResponseBodyAdvice grVoidResponseBodyAdvice() {
|
||||
GrVoidResponseBodyAdvice voidResponseBodyAdvice = new GrVoidResponseBodyAdvice();
|
||||
CopyOnWriteArrayList<ResponseBodyAdvicePredicate> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
|
||||
copyOnWriteArrayList.add(voidResponseBodyAdvice);
|
||||
voidResponseBodyAdvice.setPredicates(copyOnWriteArrayList);
|
||||
voidResponseBodyAdvice.setResponseBodyAdviceProcessor(voidResponseBodyAdvice);
|
||||
return voidResponseBodyAdvice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理前回调(目前仅打印异常日志)
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public BeforeControllerAdviceProcess beforeControllerAdviceProcess() {
|
||||
return new DefaultBeforeControllerAdviceProcessImpl(globalResponseProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 框架异常处理器
|
||||
*/
|
||||
@Bean
|
||||
public FrameworkExceptionAdvice frameworkExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess) {
|
||||
FrameworkExceptionAdvice frameworkExceptionAdvice = new FrameworkExceptionAdvice();
|
||||
frameworkExceptionAdvice.setRejectStrategy(new DefaultRejectStrategyImpl());
|
||||
frameworkExceptionAdvice.setControllerAdviceProcessor(frameworkExceptionAdvice);
|
||||
frameworkExceptionAdvice.setBeforeControllerAdviceProcess(beforeControllerAdviceProcess);
|
||||
frameworkExceptionAdvice.setControllerAdviceHttpProcessor(frameworkExceptionAdvice);
|
||||
return frameworkExceptionAdvice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据校验异常处理器
|
||||
*/
|
||||
@Bean
|
||||
public DataExceptionAdvice dataExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess) {
|
||||
DataExceptionAdvice dataExceptionAdvice = new DataExceptionAdvice();
|
||||
dataExceptionAdvice.setRejectStrategy(new DefaultRejectStrategyImpl());
|
||||
dataExceptionAdvice.setControllerAdviceProcessor(dataExceptionAdvice);
|
||||
dataExceptionAdvice.setBeforeControllerAdviceProcess(beforeControllerAdviceProcess);
|
||||
dataExceptionAdvice.setControllerAdviceHttpProcessor(dataExceptionAdvice);
|
||||
return dataExceptionAdvice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认全局异常处理器
|
||||
*/
|
||||
@Bean
|
||||
public DefaultGlobalExceptionAdvice defaultGlobalExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess) {
|
||||
DefaultGlobalExceptionAdvice advice = new DefaultGlobalExceptionAdvice();
|
||||
advice.setRejectStrategy(new DefaultRejectStrategyImpl());
|
||||
CopyOnWriteArrayList<ControllerAdvicePredicate> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
|
||||
copyOnWriteArrayList.add(advice);
|
||||
advice.setPredicates(copyOnWriteArrayList);
|
||||
advice.setControllerAdviceProcessor(advice);
|
||||
advice.setBeforeControllerAdviceProcess(beforeControllerAdviceProcess);
|
||||
advice.setControllerAdviceHttpProcessor(advice);
|
||||
return advice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认参数校验异常处理器
|
||||
*/
|
||||
@Bean
|
||||
public DefaultValidationExceptionAdvice defaultValidationExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess) {
|
||||
DefaultValidationExceptionAdvice advice = new DefaultValidationExceptionAdvice();
|
||||
advice.setRejectStrategy(new DefaultRejectStrategyImpl());
|
||||
advice.setControllerAdviceProcessor(advice);
|
||||
advice.setBeforeControllerAdviceProcess(beforeControllerAdviceProcess);
|
||||
advice.setControllerAdviceHttpProcessor(advice);
|
||||
return advice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际化支持
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = PropertiesConstants.WEB_RESPONSE, name = "i18n", havingValue = "true")
|
||||
public GrI18nResponseBodyAdvice grI18nResponseBodyAdvice() {
|
||||
GrI18nResponseBodyAdvice i18nResponseBodyAdvice = new GrI18nResponseBodyAdvice();
|
||||
CopyOnWriteArrayList<ResponseBodyAdvicePredicate> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
|
||||
copyOnWriteArrayList.add(i18nResponseBodyAdvice);
|
||||
i18nResponseBodyAdvice.setPredicates(copyOnWriteArrayList);
|
||||
i18nResponseBodyAdvice.setResponseBodyAdviceProcessor(i18nResponseBodyAdvice);
|
||||
return i18nResponseBodyAdvice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际化配置
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = PropertiesConstants.WEB_RESPONSE, name = "i18n", havingValue = "true")
|
||||
public MessageSource messageSource() {
|
||||
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
|
||||
messageSource.setBasenames("i18n", "i18n/empty-messages");
|
||||
messageSource.setDefaultEncoding("UTF-8");
|
||||
messageSource.setDefaultLocale(Locale.CHINA);
|
||||
return messageSource;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,25 +214,14 @@ public class GlobalResponseAutoConfiguration {
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际化支持
|
||||
* SpringDoc 全局响应处理器
|
||||
*
|
||||
* @return {@link ApiDocGlobalResponseHandler }
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = PropertiesConstants.WEB_RESPONSE, name = "i18n", havingValue = "true")
|
||||
public GrI18nAdvice i18nAdvice() {
|
||||
return new GrI18nAdvice();
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际化配置
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = PropertiesConstants.WEB_RESPONSE, name = "i18n", havingValue = "true")
|
||||
public MessageSource messageSource() {
|
||||
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
|
||||
messageSource.setBasenames("i18n", "i18n/empty-messages");
|
||||
messageSource.setDefaultEncoding("UTF-8");
|
||||
messageSource.setDefaultLocale(Locale.CHINA);
|
||||
return messageSource;
|
||||
@ConditionalOnMissingBean
|
||||
public ApiDocGlobalResponseHandler apiDocGlobalResponseHandler() {
|
||||
return new ApiDocGlobalResponseHandler(globalResponseProperties);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
|
||||
@@ -52,9 +52,9 @@ public class TLogServletFilter implements Filter {
|
||||
try {
|
||||
TLogWebCommon.loadInstance().preHandle(httpServletRequest);
|
||||
// 把 traceId 放入 response 的 header,为了方便有些人有这样的需求,从前端拿整条链路的 traceId
|
||||
String headerName = traceProperties.getHeaderName();
|
||||
if (CharSequenceUtil.isNotBlank(headerName)) {
|
||||
httpServletResponse.addHeader(headerName, TLogContext.getTraceId());
|
||||
String traceIdName = traceProperties.getTraceIdName();
|
||||
if (CharSequenceUtil.isNotBlank(traceIdName)) {
|
||||
httpServletResponse.addHeader(traceIdName, TLogContext.getTraceId());
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
} finally {
|
||||
|
||||
@@ -35,9 +35,9 @@ public class TraceProperties {
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* 响应头名称
|
||||
* 链路 ID 名称
|
||||
*/
|
||||
private String headerName = "traceId";
|
||||
private String traceIdName = "traceId";
|
||||
|
||||
/**
|
||||
* TLog 配置
|
||||
@@ -53,12 +53,12 @@ public class TraceProperties {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getHeaderName() {
|
||||
return headerName;
|
||||
public String getTraceIdName() {
|
||||
return traceIdName;
|
||||
}
|
||||
|
||||
public void setHeaderName(String headerName) {
|
||||
this.headerName = headerName;
|
||||
public void setTraceIdName(String traceIdName) {
|
||||
this.traceIdName = traceIdName;
|
||||
}
|
||||
|
||||
public TLogProperties getTlog() {
|
||||
|
||||
@@ -21,9 +21,7 @@ import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.server.PathContainer;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
import top.continew.starter.web.util.SpringWebUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
@@ -57,14 +55,15 @@ public class XssFilter implements Filter {
|
||||
if (servletRequest instanceof HttpServletRequest request && xssProperties.isEnabled()) {
|
||||
// 放行路由:忽略 XSS 过滤
|
||||
List<String> excludePatterns = xssProperties.getExcludePatterns();
|
||||
if (CollectionUtil.isNotEmpty(excludePatterns) && isMatchPath(request.getServletPath(), excludePatterns)) {
|
||||
if (CollectionUtil.isNotEmpty(excludePatterns) && SpringWebUtils.isMatch(request
|
||||
.getServletPath(), excludePatterns)) {
|
||||
filterChain.doFilter(request, servletResponse);
|
||||
return;
|
||||
}
|
||||
// 拦截路由:执行 XSS 过滤
|
||||
List<String> includePatterns = xssProperties.getIncludePatterns();
|
||||
if (CollectionUtil.isNotEmpty(includePatterns)) {
|
||||
if (isMatchPath(request.getServletPath(), includePatterns)) {
|
||||
if (SpringWebUtils.isMatch(request.getServletPath(), includePatterns)) {
|
||||
filterChain.doFilter(new XssServletRequestWrapper(request, xssProperties), servletResponse);
|
||||
} else {
|
||||
filterChain.doFilter(request, servletResponse);
|
||||
@@ -77,22 +76,4 @@ public class XssFilter implements Filter {
|
||||
}
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断数组中是否存在匹配的路径
|
||||
*
|
||||
* @param requestUrl 请求地址
|
||||
* @param pathPatterns 指定匹配路径
|
||||
* @return true:匹配;false:不匹配
|
||||
*/
|
||||
private static boolean isMatchPath(String requestUrl, List<String> pathPatterns) {
|
||||
for (String pattern : pathPatterns) {
|
||||
PathPattern pathPattern = PathPatternParser.defaultInstance.parse(pattern);
|
||||
PathContainer pathContainer = PathContainer.parsePath(requestUrl);
|
||||
if (pathPattern.matches(pathContainer)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ import com.feiniaojin.gracefulresponse.data.ResponseStatus;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import top.continew.starter.web.autoconfigure.response.GlobalResponseProperties;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 响应信息
|
||||
*
|
||||
@@ -32,7 +30,7 @@ import java.util.Collections;
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "响应信息")
|
||||
public class R implements Response {
|
||||
public class R<T> implements Response {
|
||||
|
||||
private static final GlobalResponseProperties PROPERTIES = SpringUtil.getBean(GlobalResponseProperties.class);
|
||||
private static final String DEFAULT_SUCCESS_CODE = PROPERTIES.getDefaultSuccessCode();
|
||||
@@ -49,7 +47,7 @@ public class R implements Response {
|
||||
/**
|
||||
* 状态信息
|
||||
*/
|
||||
@Schema(description = "状态信息", example = "操作成功")
|
||||
@Schema(description = "状态信息", example = "ok")
|
||||
private String msg;
|
||||
|
||||
/**
|
||||
@@ -68,7 +66,7 @@ public class R implements Response {
|
||||
* 响应数据
|
||||
*/
|
||||
@Schema(description = "响应数据")
|
||||
private Object data = Collections.emptyMap();
|
||||
private T data;
|
||||
|
||||
public R() {
|
||||
}
|
||||
@@ -78,7 +76,7 @@ public class R implements Response {
|
||||
this.setMsg(msg);
|
||||
}
|
||||
|
||||
public R(String code, String msg, Object data) {
|
||||
public R(String code, String msg, T data) {
|
||||
this(code, msg);
|
||||
this.data = data;
|
||||
}
|
||||
@@ -97,7 +95,7 @@ public class R implements Response {
|
||||
|
||||
@Override
|
||||
public void setPayload(Object payload) {
|
||||
this.data = payload;
|
||||
this.data = (T)payload;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -123,11 +121,11 @@ public class R implements Response {
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public Object getData() {
|
||||
public T getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(Object data) {
|
||||
public void setData(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
package top.continew.starter.web.util;
|
||||
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -35,6 +35,8 @@ import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -70,12 +72,36 @@ public class SpringWebUtils {
|
||||
/**
|
||||
* 路径是否匹配
|
||||
*
|
||||
* @param pattern 匹配模式
|
||||
* @param path 路径
|
||||
* @param patterns 匹配模式列表
|
||||
* @return 是否匹配
|
||||
* @since 2.6.0
|
||||
*/
|
||||
public static boolean isMatch(String path, List<String> patterns) {
|
||||
return patterns.stream().anyMatch(pattern -> isMatch(path, pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径是否匹配
|
||||
*
|
||||
* @param path 路径
|
||||
* @param patterns 匹配模式列表
|
||||
* @return 是否匹配
|
||||
* @since 2.6.0
|
||||
*/
|
||||
public static boolean isMatch(String path, String... patterns) {
|
||||
return Arrays.stream(patterns).anyMatch(pattern -> isMatch(path, pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径是否匹配
|
||||
*
|
||||
* @param path 路径
|
||||
* @param pattern 匹配模式
|
||||
* @return 是否匹配
|
||||
* @since 2.4.0
|
||||
*/
|
||||
public static boolean match(String pattern, String path) {
|
||||
public static boolean isMatch(String path, String pattern) {
|
||||
PathPattern pathPattern = PathPatternParser.defaultInstance.parse(pattern);
|
||||
PathContainer pathContainer = PathContainer.parsePath(path);
|
||||
return pathPattern.matches(pathContainer);
|
||||
|
||||
@@ -4,6 +4,8 @@ continew-starter.web.response:
|
||||
i18n: false
|
||||
# 响应类全名(配置后 response-style 将不再生效)
|
||||
response-class-full-name: top.continew.starter.web.model.R
|
||||
# 自定义失败 HTTP 状态码(默认:200,建议业务和通信状态码区分)
|
||||
default-http-status-code-on-error: 200
|
||||
# 自定义成功响应码(默认:0)
|
||||
default-success-code: 0
|
||||
# 自定义成功提示(默认:ok)
|
||||
|
||||
Reference in New Issue
Block a user