检查实体类是否少字段

This commit is contained in:
guoshengxiong 2025-11-26 16:52:55 +08:00
parent dedfc336cb
commit a7c3b61680
5 changed files with 632 additions and 0 deletions

View File

@ -0,0 +1,500 @@
package com.zhgd.xmgl.modules.validator;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Slf4j
@Component
public class BulkEntityValidator {
@Autowired
@Qualifier("db1DataSource")
private DataSource dataSource;
private final Map<String, List<String>> tableColumnsCache = new ConcurrentHashMap<>();
private final ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
private final MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
/**
* 应用启动时自动验证所有实体类
*/
@PostConstruct
public void autoValidateOnStartup() {
log.info("开始自动验证实体类与数据库表映射...");
try {
Map<String, ValidationResult> results = validateAllEntitiesInProject();
printValidationSummary(results);
} catch (Exception e) {
log.error("实体类映射验证失败", e);
}
}
/**
* 验证项目中所有实体类 - 返回验证结果
*/
public Map<String, ValidationResult> validateAllEntitiesInProject() {
// 常见的实体类包路径可以根据实际情况调整
String[] commonEntityPackages = {
"com.zhgd.xmgl.modules.**.entity"
};
Map<String, ValidationResult> allResults = new LinkedHashMap<>();
for (String basePackage : commonEntityPackages) {
try {
Map<String, ValidationResult> packageResults = validatePackageEntities(basePackage);
allResults.putAll(packageResults);
} catch (Exception e) {
log.warn("扫描包 {} 失败: {}", basePackage, e.getMessage());
allResults.put(basePackage, ValidationResult.error(basePackage, "扫描包失败: " + e.getMessage()));
}
}
return allResults;
}
/**
* 验证指定包下的所有实体类
*/
public Map<String, ValidationResult> validatePackageEntities(String basePackage) {
log.info("开始扫描包: {}", basePackage);
Map<String, ValidationResult> results = new LinkedHashMap<>();
Set<Class<?>> entityClasses = findEntitiesInPackage(basePackage);
log.info("在包 {} 中找到 {} 个实体类", basePackage, entityClasses.size());
for (Class<?> entityClass : entityClasses) {
try {
ValidationResult result = validateSingleEntity(entityClass);
results.put(entityClass.getSimpleName(), result);
} catch (Exception e) {
log.error("验证实体类 {} 失败: {}", entityClass.getSimpleName(), e.getMessage());
results.put(entityClass.getSimpleName(),
ValidationResult.error(entityClass.getSimpleName(), e.getMessage()));
}
}
return results;
}
/**
* 验证单个实体类
*/
public ValidationResult validateSingleEntity(Class<?> entityClass) {
TableName tableAnnotation = entityClass.getAnnotation(TableName.class);
if (tableAnnotation == null) {
return ValidationResult.error(entityClass.getSimpleName(), "缺少 @TableName 注解");
}
String tableName = tableAnnotation.value();
if (tableName.isEmpty()) {
return ValidationResult.error(entityClass.getSimpleName(), "@TableName 注解值为空");
}
// 检查表是否存在
if (!isTableExists(tableName)) {
return ValidationResult.error(entityClass.getSimpleName(),
"数据库表不存在: " + tableName);
}
List<String> dbColumns = getTableColumns(tableName);
List<EntityFieldInfo> entityFields = getEntityFields(entityClass);
// 对比字段映射
List<String> missingInEntity = findMissingInEntity(dbColumns, entityFields);
List<String> missingInDb = findMissingInDatabase(entityFields, dbColumns);
List<String> typeMismatches = findTypeMismatches(entityClass, tableName, entityFields, dbColumns);
return ValidationResult.of(entityClass.getSimpleName(), tableName,
missingInEntity, missingInDb, typeMismatches);
}
/**
* 扫描包下的所有实体类
*/
private Set<Class<?>> findEntitiesInPackage(String basePackage) {
Set<Class<?>> entities = new LinkedHashSet<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
ClassUtils.convertClassNameToResourcePath(basePackage) + "/**/*.class";
Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
for (Resource resource : resources) {
if (resource.isReadable()) {
try {
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
String className = metadataReader.getClassMetadata().getClassName();
// 过滤内部类匿名类等
if (className.contains("$")) {
continue;
}
Class<?> clazz = Class.forName(className);
if (clazz.isAnnotationPresent(TableName.class)) {
entities.add(clazz);
}
} catch (ClassNotFoundException e) {
log.warn("无法加载类: {}", resource.getFilename());
}
}
}
} catch (Exception e) {
log.error("扫描包 {} 失败: {}", basePackage, e.getMessage());
}
return entities;
}
/**
* 获取数据库表的所有字段
*/
private List<String> getTableColumns(String tableName) {
return tableColumnsCache.computeIfAbsent(tableName, this::fetchTableColumnsFromDb);
}
private List<String> fetchTableColumnsFromDb(String tableName) {
List<String> columns = new ArrayList<>();
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
// 尝试不同的表名匹配策略
ResultSet rs = metaData.getColumns(null, null, tableName, null);
if (!rs.next()) {
// 尝试小写
rs = metaData.getColumns(null, null, tableName.toLowerCase(), null);
}
if (!rs.next()) {
// 尝试大写
rs = metaData.getColumns(null, null, tableName.toUpperCase(), null);
}
// 重置游标
rs.beforeFirst();
while (rs.next()) {
String columnName = rs.getString("COLUMN_NAME");
// 清理列名中的特殊字符
columnName = cleanColumnName(columnName);
columns.add(columnName.toLowerCase());
}
} catch (SQLException e) {
log.error("获取表 {} 字段失败: {}", tableName, e.getMessage());
}
return columns;
}
/**
* 检查表是否存在
*/
private boolean isTableExists(String tableName) {
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
ResultSet rs = metaData.getTables(null, null, tableName, null);
if (rs.next()) return true;
// 尝试小写
rs = metaData.getTables(null, null, tableName.toLowerCase(), null);
if (rs.next()) return true;
// 尝试大写
rs = metaData.getTables(null, null, tableName.toUpperCase(), null);
return rs.next();
} catch (SQLException e) {
log.error("检查表是否存在失败: {}", e.getMessage());
return false;
}
}
/**
* 获取实体类的所有字段信息
*/
private List<EntityFieldInfo> getEntityFields(Class<?> entityClass) {
List<EntityFieldInfo> fields = new ArrayList<>();
Class<?> currentClass = entityClass;
// 遍历所有父类直到 Object
while (currentClass != null && currentClass != Object.class) {
Field[] declaredFields = currentClass.getDeclaredFields();
for (Field field : declaredFields) {
// 忽略静态字段序列化字段等
if (shouldIgnoreField(field)) {
continue;
}
EntityFieldInfo fieldInfo = extractFieldInfo(field);
if (fieldInfo != null) {
fields.add(fieldInfo);
}
}
currentClass = currentClass.getSuperclass();
}
return fields;
}
/**
* 判断是否应该忽略该字段
*/
private boolean shouldIgnoreField(Field field) {
int modifiers = field.getModifiers();
// 忽略静态字段
if (Modifier.isStatic(modifiers)) {
return true;
}
// 忽略序列化字段
if ("serialVersionUID".equals(field.getName())) {
return true;
}
// 忽略被 @TableField(exist = false) 标记的字段
TableField tableField = field.getAnnotation(TableField.class);
if (tableField != null && !tableField.exist()) {
return true;
}
return false;
}
/**
* 提取字段信息
*/
private EntityFieldInfo extractFieldInfo(Field field) {
String fieldName = field.getName();
String columnName;
TableField tableField = field.getAnnotation(TableField.class);
if (tableField != null && !tableField.value().isEmpty()) {
columnName = tableField.value();
} else {
columnName = StrUtil.toUnderlineCase(fieldName);
}
// 清理列名中的特殊字符
columnName = cleanColumnName(columnName);
return new EntityFieldInfo(
fieldName,
columnName.toLowerCase(),
field.getType().getSimpleName()
);
}
/**
* 清理列名处理各种特殊情况
*/
private String cleanColumnName(String columnName) {
if (columnName == null || columnName.isEmpty()) {
return columnName;
}
// 去掉反引号单引号双引号等
columnName = columnName.replace("`", "")
.replace("'", "")
.replace("\"", "")
.trim();
// 如果列名被方括号包围SQL Server风格去掉方括号
if (columnName.startsWith("[") && columnName.endsWith("]")) {
columnName = columnName.substring(1, columnName.length() - 1);
}
return columnName;
}
/**
* 找出数据库中有的但实体类缺少的字段
*/
private List<String> findMissingInEntity(List<String> dbColumns, List<EntityFieldInfo> entityFields) {
Set<String> entityColumnNames = entityFields.stream()
.map(EntityFieldInfo::getColumnName)
.collect(Collectors.toSet());
return dbColumns.stream()
.filter(column -> !entityColumnNames.contains(column))
.collect(Collectors.toList());
}
/**
* 找出实体类有的但数据库缺少的字段
*/
private List<String> findMissingInDatabase(List<EntityFieldInfo> entityFields, List<String> dbColumns) {
Set<String> dbColumnSet = new HashSet<>(dbColumns);
return entityFields.stream()
.map(EntityFieldInfo::getColumnName)
.filter(columnName -> !dbColumnSet.contains(columnName))
.collect(Collectors.toList());
}
/**
* 找出类型不匹配的字段基础实现
*/
private List<String> findTypeMismatches(Class<?> entityClass, String tableName,
List<EntityFieldInfo> entityFields, List<String> dbColumns) {
// 这里可以实现更复杂的类型匹配检查
// 目前返回空列表可以根据需要扩展
return new ArrayList<>();
}
/**
* 打印验证摘要
*/
public void printValidationSummary(Map<String, ValidationResult> results) {
if (results.isEmpty()) {
log.info("🎉 没有找到需要验证的实体类!");
return;
}
long errorCount = results.values().stream()
.filter(ValidationResult::hasIssues)
.count();
long successCount = results.size() - errorCount;
log.info("验证完成!总计验证 {} 个实体类,成功: {},存在问题: {}",
results.size(), successCount, errorCount);
if (errorCount == 0) {
log.info("🎉 所有实体类映射验证通过!");
return;
}
log.warn("⚠️ 发现 {} 个实体类存在映射问题:", errorCount);
results.forEach((className, result) -> {
if (result.hasIssues()) {
log.warn("\n==========================================");
log.warn("实体类: {} -> 表: {}", className, result.getTableName());
if (!result.getMissingInEntity().isEmpty()) {
log.warn("❌ 数据库有但实体缺少: {}", result.getMissingInEntity());
}
if (!result.getMissingInDb().isEmpty()) {
log.warn("❌ 实体有但数据库缺少: {}", result.getMissingInDb());
}
if (!result.getTypeMismatches().isEmpty()) {
log.warn("⚠️ 类型不匹配: {}", result.getTypeMismatches());
}
if (result.getError() != null) {
log.warn("💥 验证错误: {}", result.getError());
}
}
});
log.warn("\n==========================================");
}
/**
* 生成详细的验证报告
*/
public String generateDetailedReport() {
Map<String, ValidationResult> results = validateAllEntitiesInProject();
StringBuilder report = new StringBuilder();
report.append("实体类映射验证报告\n");
report.append("生成时间: ").append(new Date()).append("\n\n");
if (results.isEmpty()) {
report.append("✅ 没有找到需要验证的实体类\n");
return report.toString();
}
long errorCount = results.values().stream()
.filter(ValidationResult::hasIssues)
.count();
long successCount = results.size() - errorCount;
report.append("验证统计: 总计 ").append(results.size())
.append(" 个实体类,成功: ").append(successCount)
.append(",存在问题: ").append(errorCount).append("\n\n");
if (errorCount == 0) {
report.append("✅ 所有实体类映射正确\n");
return report.toString();
}
report.append("发现 ").append(errorCount).append(" 个问题:\n\n");
results.forEach((className, result) -> {
if (result.hasIssues()) {
report.append("实体类: ").append(className)
.append(" -> 表: ").append(result.getTableName()).append("\n");
if (!result.getMissingInEntity().isEmpty()) {
report.append(" 数据库有但实体缺少: ").append(result.getMissingInEntity()).append("\n");
}
if (!result.getMissingInDb().isEmpty()) {
report.append(" 实体有但数据库缺少: ").append(result.getMissingInDb()).append("\n");
}
if (!result.getTypeMismatches().isEmpty()) {
report.append(" 类型不匹配: ").append(result.getTypeMismatches()).append("\n");
}
if (result.getError() != null) {
report.append(" 错误: ").append(result.getError()).append("\n");
}
report.append("\n");
}
});
return report.toString();
}
/**
* 获取验证统计信息
*/
public Map<String, Object> getValidationStatistics() {
Map<String, ValidationResult> results = validateAllEntitiesInProject();
long total = results.size();
long errorCount = results.values().stream()
.filter(ValidationResult::hasIssues)
.count();
long successCount = total - errorCount;
Map<String, Object> stats = new HashMap<>();
stats.put("totalEntities", total);
stats.put("successCount", successCount);
stats.put("errorCount", errorCount);
stats.put("successRate", total > 0 ? (successCount * 100.0 / total) : 0);
stats.put("lastValidated", new Date());
return stats;
}
}

View File

@ -0,0 +1,15 @@
package com.zhgd.xmgl.modules.validator;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 实体字段信息类
*/
@Data
@AllArgsConstructor
class EntityFieldInfo {
private String fieldName;
private String columnName;
private String fieldType;
}

View File

@ -0,0 +1,62 @@
package com.zhgd.xmgl.modules.validator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/xmgl/entity-validator")
public class EntityValidatorController {
@Autowired
private BulkEntityValidator validator;
@GetMapping("/validate-all")
public Map<String, Object> validateAll() {
Map<String, ValidationResult> results = validator.validateAllEntitiesInProject();
long errorCount = results.values().stream()
.filter(ValidationResult::hasIssues)
.count();
Map<String, Object> response = new HashMap<>();
response.put("totalEntities", results.size());
response.put("errorCount", errorCount);
response.put("successCount", results.size() - errorCount);
response.put("results", results);
response.put("report", validator.generateDetailedReport());
return response;
}
@PostMapping("/validate-package")
public Map<String, Object> validatePackage(@RequestParam String packageName) {
Map<String, ValidationResult> results = validator.validatePackageEntities(packageName);
long errorCount = results.values().stream()
.filter(ValidationResult::hasIssues)
.count();
Map<String, Object> response = new HashMap<>();
response.put("package", packageName);
response.put("totalEntities", results.size());
response.put("errorCount", errorCount);
response.put("successCount", results.size() - errorCount);
response.put("results", results);
response.put("hasIssues", errorCount > 0);
return response;
}
@GetMapping("/status")
public Map<String, Object> getValidationStatus() {
return validator.getValidationStatistics();
}
@GetMapping("/report")
public String getDetailedReport() {
return validator.generateDetailedReport();
}
}

View File

@ -0,0 +1,40 @@
package com.zhgd.xmgl.modules.validator;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Collections;
import java.util.List;
/**
* 验证结果类
*/
@Data
@AllArgsConstructor
class ValidationResult {
private String className;
private String tableName;
private List<String> missingInEntity;
private List<String> missingInDb;
private List<String> typeMismatches;
private String error;
public static ValidationResult of(String className, String tableName,
List<String> missingInEntity, List<String> missingInDb,
List<String> typeMismatches) {
return new ValidationResult(className, tableName, missingInEntity, missingInDb, typeMismatches, null);
}
public static ValidationResult error(String className, String error) {
return new ValidationResult(className, null, Collections.emptyList(),
Collections.emptyList(), Collections.emptyList(), error);
}
public boolean hasIssues() {
return error != null ||
!missingInEntity.isEmpty() ||
!missingInDb.isEmpty() ||
!typeMismatches.isEmpty();
}
}

View File

@ -0,0 +1,15 @@
package com.zhgd.xmgl.modules.validator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ValidatorConfig {
@Bean
@ConditionalOnMissingBean
public BulkEntityValidator bulkEntityValidator() {
return new BulkEntityValidator();
}
}