Commit 969c28f6 by zhu.zewen

优化数据库相关错误处理

parent 13706a12
package com.jmai.sys.config.db;
import com.jmai.sys.exception.SqlExecutionInterceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collection;
/**
* MyBatis拦截器配置类,用于注册自定义SQL执行拦截器
*/
@Configuration
public class MybatisInterceptorConfig {
@Resource
private Collection<SqlSessionFactory> sqlSessionFactoryList;
@PostConstruct
public void addMybatisInterceptor() {
SqlExecutionInterceptor sqlExecutionInterceptor = new SqlExecutionInterceptor();
sqlSessionFactoryList.forEach(sqlSessionFactory ->
sqlSessionFactory.getConfiguration().addInterceptor(sqlExecutionInterceptor));
}
}
\ No newline at end of file
......@@ -47,9 +47,8 @@ public class InfynovaProperties {
@ApiModelProperty("每个路由最大连接数:100(默认)")
private Integer restTemplateMaxConnectionPerRoute = 20;
/********************************** 页面配置 **********************************/
@ApiModelProperty("页面配置-选项查询器类型:0 - 前端调用(默认),1 - 后端调用")
private Integer pageConfigOptionFetcherType = 1;
@ApiModelProperty("是否开启数据库异常处理:true - 开启(默认),false - 关闭")
private Boolean handleDatabaseExceptionCurrentSqlDispEnabled = true;
/********************************** 文件上传校验 **********************************/
/**
......@@ -84,42 +83,4 @@ public class InfynovaProperties {
*/
private Integer maxExport = 20000;
/********************************** IVS **********************************/
@ApiModelProperty(value = "业务数据存储是否开启:true - 开启,false - 关闭(默认)")
private Boolean bizDataStoreEnabled = false;
@ApiModelProperty(value = "业务数据存储类型:空 - 不存储(默认),snapshot - 快照(直接恢复),view - 视图(按逻辑还原)")
private String bizDataStoreType = "";
@ApiModelProperty("业务数据存储源:空 - 无需存储(默认),database - 数据库存储,thirdParty - 第三方存储")
private String bizDataStoreSource = "";
@ApiModelProperty("允许删除本地数据库的业务单相关业务数据:true - 允许(仅测试用),false - 不允许(默认)")
private Boolean deleteOrderInLocalEnabled = false;
/********************************** OCR **********************************/
@ApiModelProperty("镭射码识别置信区间下限:0.90(默认)")
private Double minLaserOcrScore = 90D;
@ApiModelProperty("无镭射码(无钉):true - 开启(默认),false - 关闭")
private Boolean noLaserStatusEnabled = true;
@ApiModelProperty("镭射码识别消费者数量:1个消费者(默认)")
private Integer laserRecognizeConsumerPoolSize = 2;
@ApiModelProperty("镭射码识别任务重试判断表达式:空 - 不重试(默认)")
private String laserRecognizeTaskRetryFilter = "";
@ApiModelProperty("镭射码识别任务重试次数:0 - 不重试(默认)")
private Integer laserRecognizeTaskRetryCount = 1;
@ApiModelProperty("镭射码识别任务重试触发条件:5 - 积累待处理任务超5个就直接使用重试识别(默认)")
private Integer laserRecognizeTaskRetryCondition = 5;
@ApiModelProperty("镭射码识别丢弃时间(毫秒):60秒(默认)")
private Long laserRecognizeTaskDiscardInMillis = TimeUnit.SECONDS.toMillis(60);
@ApiModelProperty("镭射码识别超时时间(毫秒):90秒(默认)")
private Long laserRecognizeTimeoutInMillis = TimeUnit.SECONDS.toMillis(90);
/********************************** OPEN **********************************/
@ApiModelProperty("开启OpenAPI:true - 开启,false - 关闭(默认)")
private Boolean openapiEnabled = true;
/********************************** SYNC **********************************/
}
package com.jmai.sys.config.web;
import com.jmai.sys.exception.SqlContextHolder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
public class WebConfig {
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
/**
* SQL上下文清理过滤器,确保每次请求结束后清理SQL上下文
*/
@Bean
public Filter sqlContextCleanupFilter() {
return new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} finally {
// 请求处理完成后清理SQL上下文,防止内存泄漏
SqlContextHolder.clearCurrentSql();
}
}
};
}
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ package com.jmai.sys.exception;
import cn.hutool.core.util.ObjectUtil;
import com.jmai.api.base.ServiceCode;
import com.jmai.api.exception.ServiceException;
import com.jmai.sys.config.properties.InfynovaProperties;
import com.jmai.sys.dto.ResponseData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.HttpRequestMethodNotSupportedException;
......@@ -14,8 +15,15 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.FileNotFoundException;
import java.sql.SQLException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.dao.DataAccessException;
import org.mybatis.spring.MyBatisSystemException;
import static com.jmai.sys.exception.ErrorCode.*;
......@@ -28,6 +36,8 @@ import static com.jmai.sys.exception.ErrorCode.*;
@Slf4j
@RestControllerAdvice
public class ServiceExceptionHandler {
@Resource
private InfynovaProperties infynovaProperties;
private ResponseData buildResponse(ServiceCode serviceCode, Throwable cause) {
ServiceCode code = ObjectUtil.isEmpty(cause) ?
......@@ -76,11 +86,94 @@ public class ServiceExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseData handleException(MethodArgumentNotValidException e, HttpServletResponse response) {
log.error("Request params error: {}", e.getMessage(), e);
response.setStatus(SYSTEM_501.getCode());
return ResponseData.error(SYSTEM_501.getCode(), e.getBindingResult().getFieldError().getDefaultMessage());
}
/**
* 数据库相关异常处理
*
* @param e
* @param response
* @return
*/
@ExceptionHandler({SQLException.class, DataAccessException.class, MyBatisSystemException.class})
public ResponseData handleDatabaseException(Exception e, HttpServletResponse response) {
log.error("Database error: {}", e.getMessage(), e);
response.setStatus(SYSTEM_500.getCode());
String errorMessage = e.getMessage();
// 获取当前执行的SQL语句
String currentSql = infynovaProperties.getHandleDatabaseExceptionCurrentSqlDispEnabled() ?
SqlContextHolder.getCurrentSql() : "";
if (errorMessage != null && errorMessage.contains("Unknown column")) {
// 处理 "Unknown column 'xxx' in 'field list'" 错误
String detailedMessage = extractColumnInfo(errorMessage);
String fullMessage = "数据库字段错误:" + detailedMessage;
if (ObjectUtil.isNotEmpty(currentSql)) {
fullMessage += ",执行的SQL语句:" + currentSql;
}
return ResponseData.error(500, fullMessage);
} else if (errorMessage != null && errorMessage.contains("doesn't exist")) {
// 处理表或数据库不存在的错误
String detailedMessage = extractTableInfo(errorMessage);
String fullMessage = "数据库对象不存在:" + detailedMessage;
if (ObjectUtil.isNotEmpty(currentSql)) {
fullMessage += ",执行的SQL语句:" + currentSql;
}
return ResponseData.error(500, fullMessage);
} else {
String fullMessage = e.getMessage();
if (ObjectUtil.isNotEmpty(currentSql)) {
fullMessage = "数据库错误:" + fullMessage + ",执行的SQL语句:" + currentSql;
}
return ResponseData.error(500, fullMessage);
}
}
/**
* 从错误信息中提取字段相关信息
* @param errorMessage 原始错误信息
* @return 包含字段详情的错误信息
*/
private String extractColumnInfo(String errorMessage) {
// 匹配 "Unknown column 'column_name' in 'field list'" 或类似格式
Pattern pattern = Pattern.compile("Unknown column '([^']+)' in '([^']+)'");
Matcher matcher = pattern.matcher(errorMessage);
if (matcher.find()) {
String columnName = matcher.group(1);
String context = matcher.group(2); // 如 'field list', 'where clause', 'order clause' 等
return String.format("未知字段 '%s' 在 %s 中:%s", columnName, context, errorMessage);
}
// 处理其他可能的格式,例如 "Unknown column 'column_name'"
Pattern simplePattern = Pattern.compile("Unknown column '([^']+)'");
Matcher simpleMatcher = simplePattern.matcher(errorMessage);
if (simpleMatcher.find()) {
String columnName = simpleMatcher.group(1);
return String.format("未知字段 '%s':%s", columnName, errorMessage);
}
return errorMessage;
}
/**
* 从错误信息中提取表相关信息
* @param errorMessage 原始错误信息
* @return 包含表详情的错误信息
*/
private String extractTableInfo(String errorMessage) {
// 匹配 "Table 'database.table_name' doesn't exist" 或类似格式
Pattern pattern = Pattern.compile("Table '([^']+)' doesn't exist");
Matcher matcher = pattern.matcher(errorMessage);
if (matcher.find()) {
String fullTableName = matcher.group(1);
return String.format("表 '%s' 不存在:%s", fullTableName, errorMessage);
}
return errorMessage;
}
/**
* 系统异常
*
* @param e
......
package com.jmai.sys.exception;
/**
* SQL上下文持有者,用于在线程范围内存储当前执行的SQL语句
*/
public class SqlContextHolder {
private static final ThreadLocal<String> SQL_HOLDER = new ThreadLocal<>();
/**
* 设置当前线程的SQL语句
* @param sql SQL语句
*/
public static void setCurrentSql(String sql) {
SQL_HOLDER.set(sql);
}
/**
* 获取当前线程的SQL语句
* @return SQL语句
*/
public static String getCurrentSql() {
return SQL_HOLDER.get();
}
/**
* 清除当前线程的SQL语句
*/
public static void clearCurrentSql() {
SQL_HOLDER.remove();
}
}
\ No newline at end of file
package com.jmai.sys.exception;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;
/**
* SQL执行拦截器,用于捕获执行的SQL语句
*/
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlExecutionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
// 获取实际的SQL语句
String sql = boundSql.getSql();
// 存储到ThreadLocal中,以便异常处理时使用
SqlContextHolder.setCurrentSql(sql);
try {
// 执行原方法
return invocation.proceed();
} finally {
// 清除ThreadLocal中的SQL,防止内存泄漏
// 注意:这里不立即清除,因为异常可能在后续处理中发生
// 我们将在适当的时候清理,比如请求结束时
}
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
// 不需要设置属性
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment