feat: 增加异常分析功能

通过实现ExceptionAnalyzer拓展异常分析
This commit is contained in:
zhouhao
2025-01-24 13:53:01 +08:00
parent fe7a6f6ca2
commit 42aff281fa
14 changed files with 322 additions and 14 deletions

View File

@@ -2,9 +2,7 @@ package org.hswebframework.web.crud.configuration;
import lombok.SneakyThrows;
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.*;
public class DialectProviders {
private static final Map<String, DialectProvider> allSupportedDialect = new HashMap<>();
@@ -19,6 +17,10 @@ public class DialectProviders {
}
}
public static List<DialectProvider> all(){
return new ArrayList<>(allSupportedDialect.values());
}
@SneakyThrows
public static DialectProvider lookup(String dialect) {
DialectProvider provider = allSupportedDialect.get(dialect);

View File

@@ -28,6 +28,7 @@ import org.hswebframework.web.crud.generator.*;
import org.hswebframework.web.crud.query.DefaultQueryHelper;
import org.hswebframework.web.crud.query.QueryHelper;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfiguration;

View File

@@ -0,0 +1,48 @@
package org.hswebframework.web.crud.exception;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.crud.configuration.DialectProvider;
import org.hswebframework.web.crud.configuration.DialectProviders;
import org.hswebframework.web.exception.analyzer.ExceptionAnalyzerReporter;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Slf4j
public class DatabaseExceptionAnalyzerReporter extends ExceptionAnalyzerReporter {
public DatabaseExceptionAnalyzerReporter() {
init();
}
void init() {
addSimpleReporter(
Pattern.compile("^Binding.*"),
error -> log
.warn(wrapLog("请在application.yml中正确配置`easyorm.dialect`,可选项为:{}"),
DialectProviders
.all()
.stream()
.map(DialectProvider::name)
.collect(Collectors.toList())
, error));
addSimpleReporter(
Pattern.compile("^Unknown database.*"),
error -> log
.warn(wrapLog("请先手动创建数据库或者配置`easyorm.default-schema`,数据库名不能包含只能由`数字字母下划线`组成."), error));
initForPgsql();
}
void initForPgsql() {
addSimpleReporter(
Pattern.compile(".*\\[3D000].*"),
error -> log
.warn(wrapLog("请先手动创建数据库,数据库名不能包含只能由`数字字母下划线`组成."), error));
addSimpleReporter(
Pattern.compile(".*\\[3F000].*"),
error -> log
.warn(wrapLog("请正确配置`easyorm.default-schema`为pgsql数据库中对应的schema."), error));
}
}

View File

@@ -10,6 +10,7 @@ import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrapper;
import org.hswebframework.web.api.crud.entity.TransactionManagers;
import org.hswebframework.web.datasource.DataSourceHolder;
import org.hswebframework.web.datasource.R2dbcDataSource;
import org.hswebframework.web.exception.I18nSupportException;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.r2dbc.connection.ConnectionFactoryUtils;
@@ -48,6 +49,17 @@ public class DefaultR2dbcExecutor extends R2dbcReactiveSqlExecutor {
return sqlRequest;
}
@Override
protected Statement prepareStatement(Statement statement, SqlRequest request) {
try {
return super.prepareStatement(statement, request);
} catch (Throwable e) {
throw new I18nSupportException
.NoStackTrace("error.sql.prepare", e)
.withSource("sql.prepare", request);
}
}
protected void bindNull(Statement statement, int index, Class type) {
if (type == Date.class) {
type = LocalDateTime.class;
@@ -60,11 +72,12 @@ public class DefaultR2dbcExecutor extends R2dbcReactiveSqlExecutor {
}
protected void bind(Statement statement, int index, Object value) {
if (value instanceof Date) {
value = ((Date) value)
.toInstant()
.atZone(ZoneOffset.systemDefault())
.toLocalDateTime();
.toInstant()
.atZone(ZoneOffset.systemDefault())
.toLocalDateTime();
}
if (bindCustomSymbol) {
statement.bind(getBindSymbol() + (index + getBindFirstIndex()), value);
@@ -77,8 +90,8 @@ public class DefaultR2dbcExecutor extends R2dbcReactiveSqlExecutor {
protected Mono<Connection> getConnection() {
if (DataSourceHolder.isDynamicDataSourceReady()) {
return DataSourceHolder.currentR2dbc()
.flatMap(R2dbcDataSource::getNative)
.flatMap(ConnectionFactoryUtils::getConnection);
.flatMap(R2dbcDataSource::getNative)
.flatMap(ConnectionFactoryUtils::getConnection);
} else {
return ConnectionFactoryUtils.getConnection(defaultFactory);
}
@@ -116,7 +129,7 @@ public class DefaultR2dbcExecutor extends R2dbcReactiveSqlExecutor {
@Override
@Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
public Mono<Integer> update(String sql, Object... args) {
return super.update(sql,args);
return super.update(sql, args);
}
@Override
@@ -128,18 +141,18 @@ public class DefaultR2dbcExecutor extends R2dbcReactiveSqlExecutor {
@Override
@Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
public Flux<Map<String, Object>> select(String sql, Object... args) {
return super.select(sql,args);
return super.select(sql, args);
}
@Override
@Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
public <E> Flux<E> select(String sql, ResultWrapper<E, ?> wrapper) {
return super.select(sql,wrapper);
return super.select(sql, wrapper);
}
@Override
@Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
public <E> Flux<E> select(SqlRequest sqlRequest, ResultWrapper<E, ?> wrapper) {
return super.select(sqlRequest,wrapper);
return super.select(sqlRequest, wrapper);
}
}

View File

@@ -0,0 +1 @@
org.hswebframework.web.crud.exception.DatabaseExceptionAnalyzerReporter

View File

@@ -6,4 +6,5 @@ error.data_error=Data error
error.internal_server_error = Internal server error
error.tree_entity_cyclic_dependency=Cannot modify parent node as oneself or one's own child node
error.tree_entity_parent_id_not_exist=Parent node does not exist or has been deleted
error.data.find.not_found=Data not found
error.data.find.not_found=Data not found
error.sql.prepare.failed.IndexOutOfBoundsException=Execute SQL failed, try check config: `easyorm.dialect`.

View File

@@ -6,4 +6,5 @@ error.data_error=\u6570\u636E\u9519\u8BEF
error.internal_server_error=\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF
error.tree_entity_cyclic_dependency=\u4E0D\u80FD\u4FEE\u6539\u7236\u8282\u70B9\u4E3A\u81EA\u5DF1\u6216\u8005\u81EA\u5DF1\u7684\u5B50\u8282\u70B9
error.tree_entity_parent_id_not_exist=\u7236\u8282\u70B9\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664
error.data.find.not_found=\u6570\u636E\u4E0D\u5B58\u5728
error.data.find.not_found=\u6570\u636E\u4E0D\u5B58\u5728
error.sql.prepare.failed.IndexOutOfBoundsException=SQL\u6267\u884C\u5931\u8D25,\u8BF7\u5C1D\u8BD5\u68C0\u67E5`easyorm.dialect`\u914D\u7F6E.

View File

@@ -0,0 +1,38 @@
package org.hswebframework.web.crud.exception;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.Assert.*;
public class DatabaseExceptionAnalyzerReporterTest {
DatabaseExceptionAnalyzerReporter reporter=new DatabaseExceptionAnalyzerReporter();
@Test
void testBinding(){
Assertions.assertTrue(reporter.doReportException(
new IndexOutOfBoundsException("Binding index 0 when only 0 parameters are expected ")
));
}
@Test
void testUnknownDatabase(){
Assertions.assertTrue(reporter.doReportException(
new IndexOutOfBoundsException("Unknown database 'jetlinks' ")
));
}
@Test
void testPgsqlUnknownDatabase(){
Assertions.assertTrue(reporter.doReportException(
new IndexOutOfBoundsException("[3D000] database \"jetlinks22\" does not exist")
));
}
@Test
void testPgsqlUnknownSchema(){
Assertions.assertTrue(reporter.doReportException(
new IndexOutOfBoundsException("[3F000] schema \"jetlinks22\" does not exist")
));
}
}

View File

@@ -0,0 +1,27 @@
package org.hswebframework.web.exception.analyzer;
/**
* 异常分析器,用于分析异常信息. 实现此接口,并使用SPI进行拓展.
*
* <pre>{@code
*
* META-INF/services/org.hswebframework.web.exception.analyzer.ExceptionAnalyzer
*
* }</pre>
*
* @author zhouhao
* @since 4.0.18
* @see ExceptionAnalyzerReporter
*/
public interface ExceptionAnalyzer {
/**
* 执行分析
*
* @param error 异常信息
* @return 是否被处理
*/
boolean analyze(Throwable error);
}

View File

@@ -0,0 +1,81 @@
package org.hswebframework.web.exception.analyzer;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
/**
* 提供基础的异常分析器实现
*
* @author zhouhao
* @since 4.0.18
*/
@Slf4j
public class ExceptionAnalyzerReporter implements ExceptionAnalyzer {
private final List<Reporter> reporter = new CopyOnWriteArrayList<>();
public static String wrapLog(String message) {
char[] arr = new char[message.length() + 2];
Arrays.fill(arr, '=');
arr[0] = '\n';
arr[arr.length - 1] = '\n';
String line = new String(arr);
return line + message + line;
}
protected void addReporter(Predicate<Throwable> predicate,
Consumer<Throwable> reporter) {
this.reporter.add(new Reporter() {
@Override
public boolean predicate(Throwable error) {
return predicate.test(error);
}
@Override
public void report(Throwable error) {
reporter.accept(error);
}
});
}
protected void addSimpleReporter(Pattern pattern, Consumer<Throwable> reporter) {
addReporter((error) -> error.getMessage() != null
&& pattern.matcher(error.getMessage()).matches(),
reporter);
}
public boolean doReportException(Throwable failure) {
Throwable cause = failure;
while (cause != null) {
for (Reporter _reporter : this.reporter) {
if (_reporter.predicate(cause)) {
_reporter.report(cause);
return true;
}
}
cause = cause.getCause();
}
return false;
}
@Override
public boolean analyze(Throwable error) {
return doReportException(error);
}
interface Reporter {
boolean predicate(Throwable error);
void report(Throwable error);
}
}

View File

@@ -0,0 +1,47 @@
package org.hswebframework.web.exception.analyzer;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.ServiceLoader;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 异常分析器,用于分析异常信息.使用{@link ExceptionAnalyzer}进行分析拓展.
*
* @author zhouhao
* @see ExceptionAnalyzer
* @since 4.0.18
*/
@Slf4j
public class ExceptionAnalyzers {
private static final List<ExceptionAnalyzer> ANALYZER = new CopyOnWriteArrayList<>();
private ExceptionAnalyzers() {
}
static {
ServiceLoader.load(ExceptionAnalyzer.class).forEach(ANALYZER::add);
}
public static void addAnalyzer(ExceptionAnalyzer analyzer) {
log.debug("add ExceptionAnalyzer:{}", analyzer);
ANALYZER.add(analyzer);
}
public static boolean analyze(Throwable failure) {
Throwable cause = failure;
while (cause != null) {
for (ExceptionAnalyzer _analyzer : ANALYZER) {
if (_analyzer.analyze(cause)) {
return true;
}
}
cause = cause.getCause();
}
return false;
}
}

View File

@@ -0,0 +1,22 @@
package org.hswebframework.web.starter.reporter;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.exception.analyzer.ExceptionAnalyzers;
import org.springframework.beans.BeansException;
import org.springframework.boot.SpringBootExceptionReporter;
import org.springframework.context.ConfigurableApplicationContext;
@Slf4j
public class GenericExceptionReport implements SpringBootExceptionReporter {
public GenericExceptionReport(ConfigurableApplicationContext context) {
}
@Override
public boolean reportException(Throwable failure) {
return ExceptionAnalyzers.analyze(failure);
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.SpringBootExceptionReporter=\
org.hswebframework.web.starter.reporter.GenericExceptionReport

View File

@@ -0,0 +1,24 @@
package org.hswebframework.web.starter.reporter;
import org.hswebframework.web.exception.analyzer.ExceptionAnalyzers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.GenericApplicationContext;
public class GenericExceptionReportTest {
@Test
void test(){
GenericExceptionReport report = new GenericExceptionReport(
new GenericApplicationContext()
);
Assertions.assertTrue(
report.reportException(new IndexOutOfBoundsException("Binding index 0 when only 0 parameters are expected "))
);
}
}