mirror of
https://gitee.com/dromara/RuoYi-Vue-Plus.git
synced 2026-09-18 17:38:48 +08:00
Pre Merge pull request !821 from apollo丶/dev
This commit is contained in:
commit
c24f20579d
@ -0,0 +1,229 @@
|
|||||||
|
package org.dromara.common.core.utils;
|
||||||
|
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.ParameterizedType;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传属性检查工具类
|
||||||
|
* <p>
|
||||||
|
* 功能:
|
||||||
|
* 1. 判断类及其父类中是否包含 MultipartFile 类型的属性。
|
||||||
|
* 2. 支持 MultipartFile 数组 (MultipartFile[]) 判断。
|
||||||
|
* 3. 支持集合泛型判断 (如 List<MultipartFile>, Set<MultipartFile>)。
|
||||||
|
* 4. 使用 ConcurrentHashMap 缓存反射结果,提升高频调用时的性能。
|
||||||
|
* 5. 兼容 JDK 21 特性(如 Record,Record 的组件会被视为字段处理)。
|
||||||
|
*
|
||||||
|
* @author Your Name
|
||||||
|
* @since JDK 21
|
||||||
|
*/
|
||||||
|
public class MultipartFileCheckUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存:Key 为类对象,Value 为是否包含 MultipartFile
|
||||||
|
* 使用 ConcurrentHashMap 保证线程安全
|
||||||
|
*/
|
||||||
|
private static final Map<Class<?>, Boolean> CACHE = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 私有构造,防止实例化
|
||||||
|
*/
|
||||||
|
private MultipartFileCheckUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查指定类是否包含 MultipartFile 属性(包含父类递归检查)
|
||||||
|
* <p>
|
||||||
|
* 此方法会自动使用缓存,重复调用同一 Class 时性能极高。
|
||||||
|
*
|
||||||
|
* @param clazz 目标类对象
|
||||||
|
* @return true 如果包含 MultipartFile(直接、数组或集合泛型),否则 false
|
||||||
|
*/
|
||||||
|
public static boolean containsMultipartFile(Class<?> clazz) {
|
||||||
|
if (clazz == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// computeIfAbsent: 如果缓存中有则直接返回,没有则执行 checkClassHierarchy 并存入缓存
|
||||||
|
return CACHE.computeIfAbsent(clazz, MultipartFileCheckUtils::checkClassHierarchy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查指定类是否包含 MultipartFile 属性(包含父类递归检查)
|
||||||
|
* <p>
|
||||||
|
* 可指定是否跳过缓存(通常用于开发调试或热更新场景)
|
||||||
|
*
|
||||||
|
* @param clazz 目标类对象
|
||||||
|
* @param useCache 是否使用缓存
|
||||||
|
* @return true 如果包含,否则 false
|
||||||
|
*/
|
||||||
|
public static boolean containsMultipartFile(Class<?> clazz, boolean useCache) {
|
||||||
|
if (clazz == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (useCache) {
|
||||||
|
return CACHE.computeIfAbsent(clazz, MultipartFileCheckUtils::checkClassHierarchy);
|
||||||
|
} else {
|
||||||
|
// 不使用缓存,直接执行检查
|
||||||
|
return checkClassHierarchy(clazz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核心逻辑:递归检查类继承体系
|
||||||
|
*
|
||||||
|
* @param clazz 目标类
|
||||||
|
* @return 检查结果
|
||||||
|
*/
|
||||||
|
private static boolean checkClassHierarchy(Class<?> clazz) {
|
||||||
|
Class<?> currentClass = clazz;
|
||||||
|
// 循环遍历当前类及其所有父类,直到 Object 类
|
||||||
|
while (currentClass != null && currentClass != Object.class) {
|
||||||
|
// 检查当前类的所有声明的字段
|
||||||
|
if (hasMultipartFileField(currentClass)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 移动到父类
|
||||||
|
currentClass = currentClass.getSuperclass();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查单个类的所有字段
|
||||||
|
*
|
||||||
|
* @param clazz 目标类
|
||||||
|
* @return true 如果该类中存在符合条件的字段
|
||||||
|
*/
|
||||||
|
private static boolean hasMultipartFileField(Class<?> clazz) {
|
||||||
|
// 获取该类声明的所有字段(public, private, protected 等)
|
||||||
|
// 注意:getDeclaredFields 不会包含父类字段,父类由外部循环处理
|
||||||
|
Field[] fields = clazz.getDeclaredFields();
|
||||||
|
|
||||||
|
// JDK 21+ 推荐使用 Stream 流式处理,代码更简洁
|
||||||
|
return Arrays.stream(fields)
|
||||||
|
.anyMatch(MultipartFileCheckUtils::isMultipartFileField);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断单个字段是否为 MultipartFile 类型(包括数组、集合等复杂情况)
|
||||||
|
*
|
||||||
|
* @param field 待检查的字段
|
||||||
|
* @return true 如果是文件上传字段
|
||||||
|
*/
|
||||||
|
private static boolean isMultipartFileField(Field field) {
|
||||||
|
Class<?> fieldType = field.getType();
|
||||||
|
|
||||||
|
// 1. 基础检查:直接是 MultipartFile 类型
|
||||||
|
if (MultipartFile.class.isAssignableFrom(fieldType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 数组检查:是否是 MultipartFile[]
|
||||||
|
if (fieldType.isArray()) {
|
||||||
|
Class<?> componentType = fieldType.getComponentType();
|
||||||
|
if (MultipartFile.class.isAssignableFrom(componentType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 集合/接口检查:如 List<MultipartFile>, Set<MultipartFile>, Collection<MultipartFile>
|
||||||
|
// 注意:不直接检查 Map,因为 Spring MVC 通常用 List/Array 接收多文件
|
||||||
|
if (Iterable.class.isAssignableFrom(fieldType) || Collection.class.isAssignableFrom(fieldType)) {
|
||||||
|
return isGenericParameterMultipartFile(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查集合字段的泛型参数是否为 MultipartFile
|
||||||
|
* <p>
|
||||||
|
* 例如:private List<MultipartFile> files;
|
||||||
|
* 我们需要获取 List 尖括号里的类型,判断它是不是 MultipartFile
|
||||||
|
*
|
||||||
|
* @param field 字段
|
||||||
|
* @return true 如果泛型类型是 MultipartFile
|
||||||
|
*/
|
||||||
|
private static boolean isGenericParameterMultipartFile(Field field) {
|
||||||
|
Type genericType = field.getGenericType();
|
||||||
|
|
||||||
|
// 必须是参数化类型(即带泛型的类型,如 List<String>)
|
||||||
|
if (genericType instanceof ParameterizedType parameterizedType) {
|
||||||
|
// 获取实际的泛型参数数组,例如 List<String> -> [String.class]
|
||||||
|
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
|
||||||
|
|
||||||
|
if (actualTypeArguments.length > 0) {
|
||||||
|
// 取第一个泛型参数(通常集合只有一个参数)
|
||||||
|
Type firstArg = actualTypeArguments[0];
|
||||||
|
|
||||||
|
// 处理通配符情况(虽然少见,但在反射中可能出现)
|
||||||
|
// 这里简化处理,只判断 Class 对象
|
||||||
|
if (firstArg instanceof Class<?> rawType) {
|
||||||
|
return MultipartFile.class.isAssignableFrom(rawType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 测试用例 ========================
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("=== 测试开始 ===");
|
||||||
|
|
||||||
|
// 1. 测试普通对象
|
||||||
|
System.out.println("SimpleDTO: " + containsMultipartFile(SimpleDTO.class)); // true
|
||||||
|
|
||||||
|
// 2. 测试数组对象
|
||||||
|
System.out.println("ArrayDTO: " + containsMultipartFile(ArrayDTO.class)); // true
|
||||||
|
|
||||||
|
// 3. 测试集合泛型对象
|
||||||
|
System.out.println("ListDTO: " + containsMultipartFile(ListDTO.class)); // true
|
||||||
|
|
||||||
|
// 4. 测试不包含文件的对象
|
||||||
|
System.out.println("NoneFileDTO: " + containsMultipartFile(NoneFileDTO.class)); // false
|
||||||
|
|
||||||
|
// 5. 测试继承关系
|
||||||
|
System.out.println("ChildDTO (继承自 SimpleDTO): " + containsMultipartFile(ChildDTO.class)); // true
|
||||||
|
|
||||||
|
// 6. 性能测试(验证缓存)
|
||||||
|
long start = System.nanoTime();
|
||||||
|
for (int i = 0; i < 10000; i++) {
|
||||||
|
containsMultipartFile(SimpleDTO.class);
|
||||||
|
}
|
||||||
|
long end = System.nanoTime();
|
||||||
|
System.out.println("10000次调用耗时 (毫秒): " + (end - start) / 1_000_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 模拟测试类 ========================
|
||||||
|
|
||||||
|
static class SimpleDTO {
|
||||||
|
private String name;
|
||||||
|
private MultipartFile file; // 包含
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ArrayDTO {
|
||||||
|
private MultipartFile[] files; // 包含数组
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ListDTO {
|
||||||
|
// 包含集合泛型
|
||||||
|
private List<MultipartFile> fileList;
|
||||||
|
private Set<MultipartFile> fileSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class NoneFileDTO {
|
||||||
|
private String name;
|
||||||
|
private Integer age;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ChildDTO extends SimpleDTO {
|
||||||
|
private String childName;
|
||||||
|
// 继承了父类的 file 字段,应该返回 true
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@ import org.dromara.common.core.constant.GlobalConstants;
|
|||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
import org.dromara.common.core.exception.ServiceException;
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
import org.dromara.common.core.utils.MessageUtils;
|
import org.dromara.common.core.utils.MessageUtils;
|
||||||
|
import org.dromara.common.core.utils.MultipartFileCheckUtils;
|
||||||
import org.dromara.common.core.utils.ServletUtils;
|
import org.dromara.common.core.utils.ServletUtils;
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
||||||
@ -111,9 +112,12 @@ public class RepeatSubmitAspect {
|
|||||||
}
|
}
|
||||||
for (Object o : paramsArray) {
|
for (Object o : paramsArray) {
|
||||||
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
|
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
|
||||||
|
// 校验对象属性为文件的情况 前端FormData 包含一般属性及文件属性 无法序列化
|
||||||
|
if(!MultipartFileCheckUtils.containsMultipartFile(o.getClass()) ){
|
||||||
params.add(JsonUtils.toJsonString(o));
|
params.add(JsonUtils.toJsonString(o));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return params.toString();
|
return params.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
package org.dromara.common.mybatis.config;
|
package org.dromara.common.mybatis.config;
|
||||||
|
|
||||||
import cn.hutool.core.net.NetUtil;
|
import cn.hutool.core.net.NetUtil;
|
||||||
|
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
|
||||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||||
import com.baomidou.mybatisplus.core.handlers.PostInitTableInfoHandler;
|
import com.baomidou.mybatisplus.core.handlers.PostInitTableInfoHandler;
|
||||||
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
|
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
|
||||||
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
|
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
|
||||||
|
import com.baomidou.mybatisplus.extension.MybatisMapWrapperFactory;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||||
@ -13,6 +15,7 @@ import org.dromara.common.core.factory.YmlPropertySourceFactory;
|
|||||||
import org.dromara.common.core.utils.SpringUtils;
|
import org.dromara.common.core.utils.SpringUtils;
|
||||||
import org.dromara.common.mybatis.aspect.DataPermissionPointcutAdvisor;
|
import org.dromara.common.mybatis.aspect.DataPermissionPointcutAdvisor;
|
||||||
import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
|
import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
|
||||||
|
import org.dromara.common.mybatis.handler.JsonbTypeHandler;
|
||||||
import org.dromara.common.mybatis.handler.MybatisExceptionHandler;
|
import org.dromara.common.mybatis.handler.MybatisExceptionHandler;
|
||||||
import org.dromara.common.mybatis.handler.PlusPostInitTableInfoHandler;
|
import org.dromara.common.mybatis.handler.PlusPostInitTableInfoHandler;
|
||||||
import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
|
import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
|
||||||
@ -24,6 +27,9 @@ import org.springframework.context.annotation.PropertySource;
|
|||||||
import org.springframework.context.annotation.Role;
|
import org.springframework.context.annotation.Role;
|
||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mybatis-plus配置类(下方注释有插件介绍)
|
* mybatis-plus配置类(下方注释有插件介绍)
|
||||||
*
|
*
|
||||||
@ -119,6 +125,26 @@ public class MybatisPlusConfig {
|
|||||||
return new PlusPostInitTableInfoHandler();
|
return new PlusPostInitTableInfoHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ConfigurationCustomizer configurationCustomizer() {
|
||||||
|
return configuration -> {
|
||||||
|
// 当使用 resultType="java.util.Map" 时,您可以通过以下步骤在 Spring Boot 中实现下划线自动转换为驼峰
|
||||||
|
configuration.setObjectWrapperFactory(new MybatisMapWrapperFactory());
|
||||||
|
|
||||||
|
// 1. 注册 Map 类型的处理器
|
||||||
|
// 实例化时传入 Map.class,这样反序列化时会自动识别为 Map
|
||||||
|
configuration.getTypeHandlerRegistry().register(Map.class, JsonbTypeHandler.class);
|
||||||
|
|
||||||
|
// 2. 注册 List 类型的处理器
|
||||||
|
// 实例化时传入 List.class,支持数组/列表结构的 JSON
|
||||||
|
configuration.getTypeHandlerRegistry().register(List.class, JsonbTypeHandler.class);
|
||||||
|
|
||||||
|
// 3. 注册 Object 类型的处理器
|
||||||
|
// 作为兜底,处理未明确指定 Map 或 List 的 Object 字段
|
||||||
|
configuration.getTypeHandlerRegistry().register(Object.class, JsonbTypeHandler.class);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PaginationInnerInterceptor 分页插件,自动识别数据库类型
|
* PaginationInnerInterceptor 分页插件,自动识别数据库类型
|
||||||
* https://baomidou.com/pages/97710a/
|
* https://baomidou.com/pages/97710a/
|
||||||
|
|||||||
@ -0,0 +1,93 @@
|
|||||||
|
package org.dromara.common.mybatis.handler;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.ibatis.type.JdbcType;
|
||||||
|
import org.apache.ibatis.type.MappedTypes;
|
||||||
|
import org.dromara.common.mybatis.helper.DataBaseHelper;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用 JSON/JSONB 字段处理器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@MappedTypes({Object.class})
|
||||||
|
public class JsonbTypeHandler extends JacksonTypeHandler {
|
||||||
|
// PGobject 的类全名
|
||||||
|
private static final String PG_OBJECT_CLASS_NAME = "org.postgresql.util.PGobject";
|
||||||
|
|
||||||
|
public JsonbTypeHandler() {
|
||||||
|
super(Object.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonbTypeHandler(Class<?> type) {
|
||||||
|
super(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写库:Java 对象 → JSON 字符串 → 数据库(PostgreSQL 使用 PGobject)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
|
||||||
|
// 1. 利用父类转 JSON 字符串
|
||||||
|
String json = super.toJson(parameter);
|
||||||
|
|
||||||
|
// 2. 检查是否为 PostgreSQL
|
||||||
|
if (DataBaseHelper.getDataBaseType().isPostgreSql()) {
|
||||||
|
// 3. 尝试使用反射处理 PGobject
|
||||||
|
boolean success = setPgObjectByReflection(ps, i, json);
|
||||||
|
// 4. 如果反射失败(比如驱动版本不对),降级为 setString
|
||||||
|
if (!success) {
|
||||||
|
ps.setString(i, json);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// MySQL 或其他数据库
|
||||||
|
ps.setString(i, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反射设置 PGobject
|
||||||
|
* <p>
|
||||||
|
* 使用 catch (Exception) 兜底,确保:
|
||||||
|
* 1. 缺少 PostgreSQL 驱动时 -> 降级
|
||||||
|
* 2. setObject 抛 SQLException 时 -> 降级
|
||||||
|
* 3. 反射调用失败时 -> 降级
|
||||||
|
*/
|
||||||
|
private boolean setPgObjectByReflection(PreparedStatement ps, int i, String json) {
|
||||||
|
try {
|
||||||
|
// 1. 加载类
|
||||||
|
Class<?> pgObjectClass = Class.forName(PG_OBJECT_CLASS_NAME);
|
||||||
|
|
||||||
|
// 2. 实例化
|
||||||
|
Object pgObject = pgObjectClass.getDeclaredConstructor().newInstance();
|
||||||
|
|
||||||
|
// 3. setType
|
||||||
|
Method setTypeMethod = pgObjectClass.getMethod("setType", String.class);
|
||||||
|
setTypeMethod.invoke(pgObject, "jsonb");
|
||||||
|
|
||||||
|
// 4. setValue
|
||||||
|
Method setValueMethod = pgObjectClass.getMethod("setValue", String.class);
|
||||||
|
setValueMethod.invoke(pgObject, json);
|
||||||
|
|
||||||
|
// 5. setObject (这里会抛 SQLException)
|
||||||
|
ps.setObject(i, pgObject);
|
||||||
|
|
||||||
|
return true; // 成功
|
||||||
|
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
// 没有驱动,是正常现象(如 MySQL 环境),吞掉异常
|
||||||
|
log.warn("PostgreSQL PGobject reflection failed, because PostgreSQL driver is missing. Error: {}", e.getMessage(), e);
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 重点:捕获所有异常,包括 SQLException, InvocationTargetException 等
|
||||||
|
// 如果 PG 处理失败,我们允许降级为 String 处理,保证业务不中断
|
||||||
|
// 生产环境建议加一行日志,方便排查为什么 PG 处理失败了
|
||||||
|
log.warn("PostgreSQL PGobject reflection failed, fallback to String. Error: {}", e.getMessage(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user