diff --git a/src/main/java/neatlogic/framework/common/config/Config.java b/src/main/java/neatlogic/framework/common/config/Config.java index 4ed2d325af5a5d6d42a8c6bbd25044cf8a5775ed..ceed926a7bbc7026508c5b6d0e3a4527b9f86b28 100644 --- a/src/main/java/neatlogic/framework/common/config/Config.java +++ b/src/main/java/neatlogic/framework/common/config/Config.java @@ -54,7 +54,7 @@ public class Config { private static String DB_HOST; private static Integer DB_PORT; private static String DB_URL; - private static String DB_TRANSACTION_TIMEOUT;// 事务超时时间 + private static Integer DB_TRANSACTION_TIMEOUT;// 事务超时时间 private static int DATASOURCE_CONNECT_TIMEOUT;//连接池连接超时时间 private static Integer DATASOURCE_MAXIMUM_POOL_SIZE;//连接数 private static Integer DATASOURCE_MAX_LIFETIME;//控制池中连接的最大生存期 @@ -62,6 +62,7 @@ public class Config { private static Integer DATASOURCE_VALIDATION_TIMEOUT;//此属性控制测试连接是否活跃的最长时间。此值必须小于 connectionTimeout private static Integer DATASOURCE_IDLE_TIMEOUT;//此属性控制允许连接在池中处于空闲状态的最长时间 private static Long DATASOURCE_KEEPALIVE_TIME;//此属性控制允许连接在池中心跳时间,不能比DATASOURCE_MAX_LIFETIME大 + private static Integer DATASOURCE_EXCEPTION_AUDIT; // 开启数据库连接获取异常日志 private static String DATA_HOME;// 存储文件路径 private static String AUDIT_HOME;// 审计日志存储文件路径 private static int SERVER_HEARTBEAT_RATE;// 心跳频率 @@ -249,7 +250,7 @@ public class Config { return DB_URL; } - public static String DB_TRANSACTION_TIMEOUT() {// root-context.xml中使用了该变量 + public static Integer DB_TRANSACTION_TIMEOUT() {// root-context.xml中使用了该变量 return DB_TRANSACTION_TIMEOUT; } @@ -281,6 +282,10 @@ public class Config { return DATASOURCE_IDLE_TIMEOUT; } + public static Integer DATASOURCE_EXCEPTION_AUDIT() { + return DATASOURCE_EXCEPTION_AUDIT; + } + public static String JMS_URL() { return JMS_URL; } @@ -628,7 +633,7 @@ public class Config { USER_EXPIRETIME = prop.getProperty("user.expiretime", "60"); LOGIN_CAPTCHA_EXPIRED_TIME = Integer.parseInt(prop.getProperty("login.captcha.expired.time", "60")); LOGIN_FAILED_TIMES_CAPTCHA = Integer.parseInt(prop.getProperty("login.failed.times.captcha", "3")); - DB_TRANSACTION_TIMEOUT = prop.getProperty("db.transaction.timeout"); + DB_TRANSACTION_TIMEOUT = Integer.parseInt(prop.getProperty("db.transaction.timeout")); DATASOURCE_CONNECT_TIMEOUT = Integer.parseInt(prop.getProperty("datasource.connect.timeout", "5000")); DATASOURCE_MAXIMUM_POOL_SIZE = Integer.parseInt(prop.getProperty("datasource.maximum.pool.size", "250")); DATASOURCE_KEEPALIVE_TIME = Long.parseLong(prop.getProperty("datasource.keepalive.time", "180000")); @@ -636,6 +641,7 @@ public class Config { DATASOURCE_MINIMUM_IDLE = Integer.parseInt(prop.getProperty("datasource.minimum.idle", "20")); DATASOURCE_VALIDATION_TIMEOUT = Integer.parseInt(prop.getProperty("datasource.validation.timeout", "5000")); DATASOURCE_IDLE_TIMEOUT = Integer.parseInt(prop.getProperty("datasource.idle.timeout", "600000")); + DATASOURCE_EXCEPTION_AUDIT = Integer.parseInt(prop.getProperty("datasource.exception.audit", "1")); DB_URL = prop.getProperty("db.url"); DB_HOST = prop.getProperty("db.host", "localhost"); DB_PORT = Integer.parseInt(prop.getProperty("db.port", "3306")); diff --git a/src/main/java/neatlogic/framework/dao/config/mybatis-config.xml b/src/main/java/neatlogic/framework/dao/config/mybatis-config.xml index ab4479288113786f6cc37ef43cd0452894bf33f4..92e89f7860ef7df471a1f814c352b614db6e9631 100644 --- a/src/main/java/neatlogic/framework/dao/config/mybatis-config.xml +++ b/src/main/java/neatlogic/framework/dao/config/mybatis-config.xml @@ -41,6 +41,7 @@ along with this program. If not, see .--> + diff --git a/src/main/java/neatlogic/framework/dao/plugin/DataSchemaInterceptor.java b/src/main/java/neatlogic/framework/dao/plugin/DataSchemaInterceptor.java index 61b93c4218a523b64078b31921e290dfa3470af0..345037bebd13d1ccb85fa96ce3bcb0b15b6cceb1 100644 --- a/src/main/java/neatlogic/framework/dao/plugin/DataSchemaInterceptor.java +++ b/src/main/java/neatlogic/framework/dao/plugin/DataSchemaInterceptor.java @@ -31,7 +31,6 @@ public class DataSchemaInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { - SqlCostInterceptor.QUERY_FROM_DATABASE_INSTANCE.set(true); StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); diff --git a/src/main/java/neatlogic/framework/dao/plugin/ExceptionAuditInterceptor.java b/src/main/java/neatlogic/framework/dao/plugin/ExceptionAuditInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..e070ce534b006e22345ac7c5132fe8ba81102543 --- /dev/null +++ b/src/main/java/neatlogic/framework/dao/plugin/ExceptionAuditInterceptor.java @@ -0,0 +1,69 @@ +/* + * + * Copyright (C) 2025 TechSure Co., Ltd. All Rights Reserved. + * This file is part of the NeatLogic software. + * Licensed under the NeatLogic Sustainable Use License (NSUL), Version 4.x – 2025. + * You may use this file only in compliance with the License. + * See the LICENSE file distributed with this work for the full license text. + * 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. + * + */ + +package neatlogic.framework.dao.plugin; + +import neatlogic.framework.common.config.Config; +import neatlogic.framework.store.mysql.SQLTransientConnectionExceptionAudit; +import neatlogic.framework.util.SnowflakeUtil; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.plugin.Intercepts; +import org.apache.ibatis.plugin.Invocation; +import org.apache.ibatis.plugin.Signature; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +import java.sql.Statement; +import java.util.Objects; + +@Intercepts({ + @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), + @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), + @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class}), + @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}), + @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), + @Signature(type = StatementHandler.class, method = "queryCursor", args = {Statement.class}), +}) +public class ExceptionAuditInterceptor implements Interceptor { + + private static final ThreadLocal mappedStatementThreadLocal = new ThreadLocal<>(); + + @Override + public Object intercept(Invocation invocation) throws Throwable { + if (Objects.equals(Config.DATASOURCE_EXCEPTION_AUDIT(), 1)) { + Object target = invocation.getTarget(); + if (target instanceof Executor) { + try { + mappedStatementThreadLocal.set((MappedStatement) invocation.getArgs()[0]); + return invocation.proceed(); + } finally { + mappedStatementThreadLocal.remove(); + } + } else { + String key = Thread.currentThread().getName() + "#" + SnowflakeUtil.uniqueLong(); + try { + MappedStatement mappedStatement = mappedStatementThreadLocal.get(); + String sqlId = mappedStatement.getId(); + SQLTransientConnectionExceptionAudit.putExecutingSQL(key, sqlId); + return invocation.proceed(); + } finally { + SQLTransientConnectionExceptionAudit.removeExecutingSQL(key); + } + } + } else { + return invocation.proceed(); + } + } +} diff --git a/src/main/java/neatlogic/framework/dao/plugin/ModifyResultMapTypeHandlerInterceptor.java b/src/main/java/neatlogic/framework/dao/plugin/ModifyResultMapTypeHandlerInterceptor.java index c5dd0e73ab5ec42728c77611f1cf939f40e3d0e2..16c8055a5663135b2eeac9e8e54168a0bb8d48b6 100644 --- a/src/main/java/neatlogic/framework/dao/plugin/ModifyResultMapTypeHandlerInterceptor.java +++ b/src/main/java/neatlogic/framework/dao/plugin/ModifyResultMapTypeHandlerInterceptor.java @@ -13,6 +13,7 @@ package neatlogic.framework.dao.plugin; import org.apache.commons.collections4.CollectionUtils; +import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ResultMap; @@ -22,6 +23,8 @@ import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.StringTypeHandler; import org.apache.ibatis.type.TypeHandler; @@ -30,59 +33,71 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.Statement; import java.util.ArrayList; import java.util.List; +import java.util.Objects; @Intercepts({ + @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) public class ModifyResultMapTypeHandlerInterceptor implements Interceptor { Logger logger = LoggerFactory.getLogger(ModifyResultMapTypeHandlerInterceptor.class); - public static final ThreadLocal mappedStatementThreadLocal = new ThreadLocal<>(); + + private static final ThreadLocal mappedStatementThreadLocal = new ThreadLocal<>(); @Override public Object intercept(Invocation invocation) throws Throwable { - try { - MappedStatement mappedStatement = mappedStatementThreadLocal.get(); - if (mappedStatement != null) { - Configuration configuration = mappedStatement.getConfiguration(); - int resultMappingSize = 0; - List resultMaps = mappedStatement.getResultMaps(); - for (ResultMap resultMap : resultMaps) { - resultMappingSize += resultMap.getResultMappings().size(); - } - if (resultMappingSize > 0) { - List longVarcharColumnLabelList = new ArrayList<>(); - PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0]; - ResultSet rs = ps.getResultSet(); - final ResultSetMetaData metaData = rs.getMetaData(); - final int columnCount = metaData.getColumnCount(); - for (int i = 1; i <= columnCount; i++) { - int columnType = metaData.getColumnType(i); - JdbcType jdbcType = JdbcType.forCode(columnType); - if (jdbcType == JdbcType.LONGVARCHAR) { - String columnLabel = metaData.getColumnLabel(i); - longVarcharColumnLabelList.add(columnLabel); - } + Method method = invocation.getMethod(); + if (Objects.equals(method.getName(), "query")) { + try { + mappedStatementThreadLocal.set((MappedStatement) invocation.getArgs()[0]); + return invocation.proceed(); + } finally { + mappedStatementThreadLocal.remove(); + } + } else { + try { + MappedStatement mappedStatement = mappedStatementThreadLocal.get(); + if (mappedStatement != null) { + Configuration configuration = mappedStatement.getConfiguration(); + int resultMappingSize = 0; + List resultMaps = mappedStatement.getResultMaps(); + for (ResultMap resultMap : resultMaps) { + resultMappingSize += resultMap.getResultMappings().size(); } - if (CollectionUtils.isNotEmpty(longVarcharColumnLabelList)) { - for (ResultMap resultMap : resultMaps) { - handleResultMap(longVarcharColumnLabelList, configuration, resultMap); + if (resultMappingSize > 0) { + List longVarcharColumnLabelList = new ArrayList<>(); + PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0]; + ResultSet rs = ps.getResultSet(); + final ResultSetMetaData metaData = rs.getMetaData(); + final int columnCount = metaData.getColumnCount(); + for (int i = 1; i <= columnCount; i++) { + int columnType = metaData.getColumnType(i); + JdbcType jdbcType = JdbcType.forCode(columnType); + if (jdbcType == JdbcType.LONGVARCHAR) { + String columnLabel = metaData.getColumnLabel(i); + longVarcharColumnLabelList.add(columnLabel); + } + } + if (CollectionUtils.isNotEmpty(longVarcharColumnLabelList)) { + for (ResultMap resultMap : resultMaps) { + handleResultMap(longVarcharColumnLabelList, configuration, resultMap); + } } } } + } catch (Exception e) { + logger.error(e.getMessage(), e); } - } catch (Exception e) { - logger.error(e.getMessage(), e); - } finally { - mappedStatementThreadLocal.remove(); + return invocation.proceed(); } - return invocation.proceed(); } private void handleResultMap(List longVarcharColumnLabelList, Configuration configuration, ResultMap resultMap) throws NoSuchFieldException, IllegalAccessException { diff --git a/src/main/java/neatlogic/framework/dao/plugin/SqlCostInterceptor.java b/src/main/java/neatlogic/framework/dao/plugin/SqlCostInterceptor.java index b5793d92a5bde472eaafd9e45607e0e1936a33d9..a6cf6a16992c0ab29639accd7df3be0d5e00f0b0 100644 --- a/src/main/java/neatlogic/framework/dao/plugin/SqlCostInterceptor.java +++ b/src/main/java/neatlogic/framework/dao/plugin/SqlCostInterceptor.java @@ -21,6 +21,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; @@ -34,6 +35,8 @@ import org.apache.ibatis.type.TypeHandlerRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.Method; +import java.sql.Connection; import java.text.DateFormat; import java.util.*; import java.util.regex.Matcher; @@ -44,13 +47,13 @@ import java.util.regex.Matcher; @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), - @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}) + @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), + @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}), }) public class SqlCostInterceptor implements Interceptor { Logger logger = LoggerFactory.getLogger(SqlCostInterceptor.class); // 判断是否查询了数据库 - public static final ThreadLocal QUERY_FROM_DATABASE_INSTANCE = new ThreadLocal<>(); - + private static final ThreadLocal QUERY_FROM_DATABASE_INSTANCE = new ThreadLocal<>(); public static class SqlIdMap { private static final Set sqlSet = new HashSet<>(); @@ -100,92 +103,97 @@ public class SqlCostInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { - MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; - ModifyResultMapTypeHandlerInterceptor.mappedStatementThreadLocal.set(mappedStatement); - long starttime = 0; - SqlAuditVo sqlAuditVo = null; - boolean hasCacheFirstLevel = false; - try { - if (!SqlIdMap.isEmpty()) { - // Object target = invocation.getTarget(); - String sqlId = mappedStatement.getId(); // 获取到节点的id,即sql语句的id - if (SqlIdMap.isExists(sqlId)) { - sqlAuditVo = new SqlAuditVo(); - if (TenantContext.get() != null) { - sqlAuditVo.setTenant(TenantContext.get().getTenantUuid()); - } - if (UserContext.get() != null) { - sqlAuditVo.setUserId(UserContext.get().getUserId()); - } - starttime = System.currentTimeMillis(); - Object parameter = null; - // 获取参数,if语句成立,表示sql语句有参数,参数格式是map形式 - if (invocation.getArgs().length > 1) { - parameter = invocation.getArgs()[1]; - } - - String sql = getSql(mappedStatement, parameter); // 获取到最终的sql语句 - //System.out.println("#############################SQL INTERCEPTOR###############################"); - //System.out.println("id:" + sqlId); - //System.out.println(sql); - sqlAuditVo.setSql(sql); - sqlAuditVo.setId(sqlId); - if (Objects.equals(invocation.getMethod().getName(), "query")) { - CacheKey key = null; - Executor executor = (Executor) invocation.getTarget(); - Object[] args = invocation.getArgs(); - if (args.length > 4) { - key = (CacheKey) args[4]; - } else if (args.length == 4) { - Object parameterObject = args[1]; - RowBounds rowBounds = (RowBounds) args[2]; - key = executor.createCacheKey(mappedStatement, parameterObject, rowBounds, mappedStatement.getBoundSql(parameterObject)); + Method method = invocation.getMethod(); + if (Objects.equals(method.getName(), "prepare")) { + QUERY_FROM_DATABASE_INSTANCE.set(true); + return invocation.proceed(); + } else { + MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; + long starttime = 0; + SqlAuditVo sqlAuditVo = null; + boolean hasCacheFirstLevel = false; + try { + if (!SqlIdMap.isEmpty()) { + // Object target = invocation.getTarget(); + String sqlId = mappedStatement.getId(); // 获取到节点的id,即sql语句的id + if (SqlIdMap.isExists(sqlId)) { + sqlAuditVo = new SqlAuditVo(); + if (TenantContext.get() != null) { + sqlAuditVo.setTenant(TenantContext.get().getTenantUuid()); + } + if (UserContext.get() != null) { + sqlAuditVo.setUserId(UserContext.get().getUserId()); + } + starttime = System.currentTimeMillis(); + Object parameter = null; + // 获取参数,if语句成立,表示sql语句有参数,参数格式是map形式 + if (invocation.getArgs().length > 1) { + parameter = invocation.getArgs()[1]; } - if (executor.isCached(mappedStatement, key)) { - hasCacheFirstLevel = true; + + String sql = getSql(mappedStatement, parameter); // 获取到最终的sql语句 + //System.out.println("#############################SQL INTERCEPTOR###############################"); + //System.out.println("id:" + sqlId); + //System.out.println(sql); + sqlAuditVo.setSql(sql); + sqlAuditVo.setId(sqlId); + if (Objects.equals(invocation.getMethod().getName(), "query")) { + CacheKey key = null; + Executor executor = (Executor) invocation.getTarget(); + Object[] args = invocation.getArgs(); + if (args.length > 4) { + key = (CacheKey) args[4]; + } else if (args.length == 4) { + Object parameterObject = args[1]; + RowBounds rowBounds = (RowBounds) args[2]; + key = executor.createCacheKey(mappedStatement, parameterObject, rowBounds, mappedStatement.getBoundSql(parameterObject)); + } + if (executor.isCached(mappedStatement, key)) { + hasCacheFirstLevel = true; + } } } } + } catch (Exception e) { + logger.error(e.getMessage(), e); } - } catch (Exception e) { - logger.error(e.getMessage(), e); - } - QUERY_FROM_DATABASE_INSTANCE.set(false); - try { - // 执行完上面的任务后,不改变原有的sql执行过程 - Object val = invocation.proceed(); - if (sqlAuditVo != null) { - if (QUERY_FROM_DATABASE_INSTANCE.get()) { - // sql语句被执行,没有使用到缓存 - sqlAuditVo.setUseCacheLevel(StringUtils.EMPTY); - } else { - if (hasCacheFirstLevel) { - sqlAuditVo.setUseCacheLevel("一级缓存"); + try { + QUERY_FROM_DATABASE_INSTANCE.set(false); + // 执行完上面的任务后,不改变原有的sql执行过程 + Object val = invocation.proceed(); + if (sqlAuditVo != null) { + if (QUERY_FROM_DATABASE_INSTANCE.get()) { + // sql语句被执行,没有使用到缓存 + sqlAuditVo.setUseCacheLevel(StringUtils.EMPTY); } else { - sqlAuditVo.setUseCacheLevel("二级缓存"); + if (hasCacheFirstLevel) { + sqlAuditVo.setUseCacheLevel("一级缓存"); + } else { + sqlAuditVo.setUseCacheLevel("二级缓存"); + } } - } - sqlAuditVo.setTimeCost(System.currentTimeMillis() - starttime); - sqlAuditVo.setRunTime(new Date()); + sqlAuditVo.setTimeCost(System.currentTimeMillis() - starttime); + sqlAuditVo.setRunTime(new Date()); - if (val != null) { - if (val instanceof List) { - sqlAuditVo.setRecordCount(((List) val).size()); - } else { - sqlAuditVo.setRecordCount(1); + if (val != null) { + if (val instanceof List) { + sqlAuditVo.setRecordCount(((List) val).size()); + } else { + sqlAuditVo.setRecordCount(1); + } } + SqlAuditManager.addSqlAudit(sqlAuditVo); + RequestContext requestContext = RequestContext.get(); + if (requestContext != null) { + requestContext.addSqlAudit(sqlAuditVo); + } + //System.out.println("time cost:" + (System.currentTimeMillis() - starttime) + "ms"); + //System.out.println("###########################################################################"); } - SqlAuditManager.addSqlAudit(sqlAuditVo); - RequestContext requestContext = RequestContext.get(); - if (requestContext != null) { - requestContext.addSqlAudit(sqlAuditVo); - } - //System.out.println("time cost:" + (System.currentTimeMillis() - starttime) + "ms"); - //System.out.println("###########################################################################"); + return val; + } finally { + QUERY_FROM_DATABASE_INSTANCE.remove(); } - return val; - } finally { - QUERY_FROM_DATABASE_INSTANCE.remove(); } } diff --git a/src/main/java/neatlogic/framework/logback/logback-base.xml b/src/main/java/neatlogic/framework/logback/logback-base.xml index de34ead75976286707ed2ad7dcfe038e0997f497..da4b6a28c45d85512407c4d6f380cd4b8adc3547 100644 --- a/src/main/java/neatlogic/framework/logback/logback-base.xml +++ b/src/main/java/neatlogic/framework/logback/logback-base.xml @@ -277,6 +277,36 @@ true + + ${log4j.home}/exceptionAudit.log + + ${log4j.home}/exceptionAudit.log.%i + 1 + 5 + + + ERROR + ACCEPT + DENY + + + 100MB + + + [%-5level]%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %logger{36}[%line] [%tenant] %requestUrl- %msg%n + + + + + + + 0 + 50 + + true + + ${log4j.home}/sqlTimeout.log @@ -357,6 +387,9 @@ + + + diff --git a/src/main/java/neatlogic/framework/store/mysql/NeatLogicBasicDataSource.java b/src/main/java/neatlogic/framework/store/mysql/NeatLogicBasicDataSource.java index 097435a98eecb9f0ace778a580937008ed63ca0e..b124e19d521038b257a979d98132804abb0c2744 100644 --- a/src/main/java/neatlogic/framework/store/mysql/NeatLogicBasicDataSource.java +++ b/src/main/java/neatlogic/framework/store/mysql/NeatLogicBasicDataSource.java @@ -18,9 +18,11 @@ import neatlogic.framework.common.util.RC4Util; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.jdbc.CannotGetJdbcConnectionException; import java.sql.Connection; import java.sql.SQLException; +import java.sql.SQLTransientConnectionException; import java.sql.Statement; import java.util.Objects; @@ -29,7 +31,13 @@ public class NeatLogicBasicDataSource extends HikariDataSource {//替换dbcp2的 @Override public Connection getConnection() throws SQLException { - Connection conn = super.getConnection(); + Connection conn = null; + try { + conn = super.getConnection(); + } catch (CannotGetJdbcConnectionException | SQLTransientConnectionException ex) { + SQLTransientConnectionExceptionAudit.audit(); + throw ex; + } conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); try (Statement statement = conn.createStatement()) { if (Objects.equals(DatasourceManager.getDatabaseId(), DatabaseVendor.MYSQL.getDatabaseId())) { diff --git a/src/main/java/neatlogic/framework/store/mysql/SQLTransientConnectionExceptionAudit.java b/src/main/java/neatlogic/framework/store/mysql/SQLTransientConnectionExceptionAudit.java new file mode 100644 index 0000000000000000000000000000000000000000..f4097ea126222248ec2b41e8e0dc936e6054bb22 --- /dev/null +++ b/src/main/java/neatlogic/framework/store/mysql/SQLTransientConnectionExceptionAudit.java @@ -0,0 +1,119 @@ +/* + * + * Copyright (C) 2025 TechSure Co., Ltd. All Rights Reserved. + * This file is part of the NeatLogic software. + * Licensed under the NeatLogic Sustainable Use License (NSUL), Version 4.x – 2025. + * You may use this file only in compliance with the License. + * See the LICENSE file distributed with this work for the full license text. + * 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. + * + */ + +package neatlogic.framework.store.mysql; + +import com.alibaba.fastjson.JSON; +import com.zaxxer.hikari.HikariPoolMXBean; +import neatlogic.framework.common.config.Config; +import neatlogic.framework.dto.healthcheck.DataSourceInfoVo; +import neatlogic.framework.util.ThreadUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class SQLTransientConnectionExceptionAudit { + + private static final Logger logger = LoggerFactory.getLogger(SQLTransientConnectionExceptionAudit.class); + + // 保存上次抛异常的时间毫秒数 + private final static AtomicLong lastThrowExceptionMillisecondsAtomicLong = new AtomicLong(0); + private final static AtomicInteger countAtomicInteger = new AtomicInteger(3); + private final static Map executingSQLMap = new ConcurrentHashMap<>(); + + public static Map getExecutingSQL() { + return new HashMap<>(executingSQLMap); + } + + public static void clearExecutingSQL() { + executingSQLMap.clear(); + } + + public static void putExecutingSQL(String key, String value) { + executingSQLMap.put(key, value); + } + + public static void removeExecutingSQL(String key) { + executingSQLMap.remove(key); + } + + /** + * 五分钟内只打印三次日志 + */ + public static void audit() { + if (Objects.equals(Config.DATASOURCE_EXCEPTION_AUDIT(), 1)) { + boolean flag = false; + long currentTimeMillis = System.currentTimeMillis(); + long lastThrowExceptionMilliseconds = lastThrowExceptionMillisecondsAtomicLong.getAndUpdate(operand -> currentTimeMillis); + long interval = currentTimeMillis - lastThrowExceptionMilliseconds; + if (interval > TimeUnit.MINUTES.toMillis(5)) { + if (countAtomicInteger.compareAndSet(3, 0)) { + flag = true; + } + } else { + int count = countAtomicInteger.updateAndGet(operand -> { + if (operand < 3) { + return operand + 1; + } else { + return operand; + } + }); + if (count < 3) { + flag = true; + } + } + if (flag) { + doAudit(); + } + } + } + + private static synchronized void doAudit() { + try { + DataSourceInfoVo dataSourceInfoVo = new DataSourceInfoVo(); + NeatLogicBasicDataSource datasource = DatasourceManager.getDatasource(); + dataSourceInfoVo.setPoolName(datasource.getPoolName()); + HikariPoolMXBean hikariPoolMXBean = datasource.getHikariPoolMXBean(); + if (hikariPoolMXBean != null) { + dataSourceInfoVo.setIdleConnections(hikariPoolMXBean.getIdleConnections()); + dataSourceInfoVo.setActiveConnections(hikariPoolMXBean.getActiveConnections()); + dataSourceInfoVo.setThreadsAwaitingConnection(hikariPoolMXBean.getThreadsAwaitingConnection()); + dataSourceInfoVo.setTotalConnections(hikariPoolMXBean.getTotalConnections()); + } + Map executingSQLSnapshotMap = new HashMap<>(executingSQLMap); + StringWriter writer = new StringWriter(); + ThreadUtil.dumpTraces(writer); + writer.write("=================正在执行的SQL语句有" + executingSQLSnapshotMap.size() + "条================="); + writer.write(System.lineSeparator()); + for (Map.Entry entry : executingSQLSnapshotMap.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + writer.write("[" + key + "] 线程正在执行 " + value); + writer.write(System.lineSeparator()); + } + writer.write("连接池信息: " + JSON.toJSONString(dataSourceInfoVo)); + Logger exceptionAuditLogger = LoggerFactory.getLogger("exceptionAudit"); + exceptionAuditLogger.error(writer.toString()); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } +} diff --git a/src/main/java/neatlogic/framework/util/ThreadUtil.java b/src/main/java/neatlogic/framework/util/ThreadUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..7e4651baea42edc4a4b3637ef7cfc5bc8fbae2b7 --- /dev/null +++ b/src/main/java/neatlogic/framework/util/ThreadUtil.java @@ -0,0 +1,71 @@ +/* + * + * Copyright (C) 2025 TechSure Co., Ltd. All Rights Reserved. + * This file is part of the NeatLogic software. + * Licensed under the NeatLogic Sustainable Use License (NSUL), Version 4.x – 2025. + * You may use this file only in compliance with the License. + * See the LICENSE file distributed with this work for the full license text. + * 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. + * + */ + +package neatlogic.framework.util; + +import neatlogic.framework.asynchronization.threadlocal.RequestContext; +import neatlogic.framework.common.config.Config; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.io.Writer; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class ThreadUtil { + + public static void dumpTraces(Writer writer) throws IOException { + ThreadMXBean mxBean = ManagementFactory.getThreadMXBean(); + ThreadInfo[] threadInfos = mxBean.getThreadInfo(mxBean.getAllThreadIds(), 0); + Map threadInfoMap = new HashMap<>(); + for (ThreadInfo threadInfo : threadInfos) { + if (threadInfo != null) { + threadInfoMap.put(threadInfo.getThreadId(), threadInfo); + } + } + Map stacks = Thread.getAllStackTraces(); + long now = System.currentTimeMillis(); + String localAddr = StringUtils.EMPTY; + String url = StringUtils.EMPTY; + RequestContext requestContext = RequestContext.get(); + if (requestContext != null) { + if (requestContext.getRequest() != null && StringUtils.isNotBlank(requestContext.getRequest().getLocalAddr())) { + localAddr = requestContext.getRequest().getLocalAddr(); + } + if (StringUtils.isNotBlank(requestContext.getUrl())) { + url = requestContext.getUrl(); + } + } + writer.write("\n=================" + stacks.size() + " thread of " + localAddr + " at " + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss z").format(new Date(now)) + " start.serverId is " + Config.SCHEDULE_SERVER_ID + "=================\n\n"); + for (Map.Entry entry : stacks.entrySet()) { + Thread thread = entry.getKey(); + writer.write("\"" + thread.getName() + "\" prio=" + thread.getPriority() + " tid=" + thread.getId() + " " + thread.getState() + " " + (thread.isDaemon() ? "deamon" : "worker")); + ThreadInfo threadInfo = threadInfoMap.get(thread.getId()); + if (threadInfo != null) { + writer.write(" native=" + threadInfo.isInNative() + ", suspended=" + threadInfo.isSuspended() + ", block=" + threadInfo.getBlockedCount() + ", wait=" + threadInfo.getWaitedCount()); + writer.write(" lock=" + threadInfo.getLockName() + " owned by " + threadInfo.getLockOwnerName() + " (" + threadInfo.getLockOwnerId() + "), cpu=" + (mxBean.getThreadCpuTime(threadInfo.getThreadId()) / 1000000L) + ", user=" + (mxBean.getThreadUserTime(threadInfo.getThreadId()) / 1000000L) + "\n"); + } + for (StackTraceElement element : entry.getValue()) { + writer.write("\t\t"); + writer.write(element.toString()); + writer.write("\n"); + } + writer.write("\n"); + } + writer.write("=================" + stacks.size() + " thread of " + url + " at " + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss z").format(new Date(now)) + " end.=================\n\n"); + } +}