mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-08-29 03:06:45 +08:00
Merge pull request #38762 from MelleD
* pr/38762: Polish 'Add conditional bean for jOOQ translator' Add conditional bean for jOOQ translator Closes gh-38762
This commit is contained in:
commit
cf8e06a7a4
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.jooq;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jooq.ExecuteContext;
|
||||
import org.jooq.SQLDialect;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
|
||||
import org.springframework.jdbc.support.SQLExceptionTranslator;
|
||||
import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ExceptionTranslatorExecuteListener} that delegates to
|
||||
* an {@link SQLExceptionTranslator}.
|
||||
*
|
||||
* @author Lukas Eder
|
||||
* @author Andreas Ahlenstorf
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
final class DefaultExceptionTranslatorExecuteListener implements ExceptionTranslatorExecuteListener {
|
||||
|
||||
// Based on the jOOQ-spring-example from https://github.com/jOOQ/jOOQ
|
||||
|
||||
private static final Log defaultLogger = LogFactory.getLog(ExceptionTranslatorExecuteListener.class);
|
||||
|
||||
private final Log logger;
|
||||
|
||||
private Function<ExecuteContext, SQLExceptionTranslator> translatorFactory;
|
||||
|
||||
DefaultExceptionTranslatorExecuteListener() {
|
||||
this(defaultLogger, new DefaultTranslatorFactory());
|
||||
}
|
||||
|
||||
DefaultExceptionTranslatorExecuteListener(Function<ExecuteContext, SQLExceptionTranslator> translatorFactory) {
|
||||
this(defaultLogger, translatorFactory);
|
||||
}
|
||||
|
||||
DefaultExceptionTranslatorExecuteListener(Log logger) {
|
||||
this(logger, new DefaultTranslatorFactory());
|
||||
}
|
||||
|
||||
private DefaultExceptionTranslatorExecuteListener(Log logger,
|
||||
Function<ExecuteContext, SQLExceptionTranslator> translatorFactory) {
|
||||
Assert.notNull(translatorFactory, "TranslatorFactory must not be null");
|
||||
this.logger = logger;
|
||||
this.translatorFactory = translatorFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exception(ExecuteContext context) {
|
||||
SQLExceptionTranslator translator = this.translatorFactory.apply(context);
|
||||
// The exception() callback is not only triggered for SQL exceptions but also for
|
||||
// "normal" exceptions. In those cases sqlException() returns null.
|
||||
SQLException exception = context.sqlException();
|
||||
while (exception != null) {
|
||||
handle(context, translator, exception);
|
||||
exception = exception.getNextException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a single exception in the chain. SQLExceptions might be nested multiple
|
||||
* levels deep. The outermost exception is usually the least interesting one ("Call
|
||||
* getNextException to see the cause."). Therefore the innermost exception is
|
||||
* propagated and all other exceptions are logged.
|
||||
* @param context the execute context
|
||||
* @param translator the exception translator
|
||||
* @param exception the exception
|
||||
*/
|
||||
private void handle(ExecuteContext context, SQLExceptionTranslator translator, SQLException exception) {
|
||||
DataAccessException translated = translator.translate("jOOQ", context.sql(), exception);
|
||||
if (exception.getNextException() != null) {
|
||||
this.logger.error("Execution of SQL statement failed.", (translated != null) ? translated : exception);
|
||||
return;
|
||||
}
|
||||
if (translated != null) {
|
||||
context.exception(translated);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default {@link SQLExceptionTranslator} factory that creates the translator based on
|
||||
* the Spring DB name.
|
||||
*/
|
||||
private static final class DefaultTranslatorFactory implements Function<ExecuteContext, SQLExceptionTranslator> {
|
||||
|
||||
@Override
|
||||
public SQLExceptionTranslator apply(ExecuteContext context) {
|
||||
return apply(context.configuration().dialect());
|
||||
}
|
||||
|
||||
private SQLExceptionTranslator apply(SQLDialect dialect) {
|
||||
String dbName = getSpringDbName(dialect);
|
||||
return (dbName != null) ? new SQLErrorCodeSQLExceptionTranslator(dbName)
|
||||
: new SQLStateSQLExceptionTranslator();
|
||||
}
|
||||
|
||||
private String getSpringDbName(SQLDialect dialect) {
|
||||
return (dialect != null && dialect.thirdParty() != null) ? dialect.thirdParty().springDbName() : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.jooq;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jooq.ExecuteContext;
|
||||
import org.jooq.ExecuteListener;
|
||||
import org.jooq.impl.DefaultExecuteListenerProvider;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.support.SQLExceptionTranslator;
|
||||
|
||||
/**
|
||||
* An {@link ExecuteListener} used by the auto-configured
|
||||
* {@link DefaultExecuteListenerProvider} to translate exceptions in the
|
||||
* {@link ExecuteContext}. Most commonly used to translate {@link SQLException
|
||||
* SQLExceptions} to Spring-specific {@link DataAccessException DataAccessExceptions} by
|
||||
* adapting an existing {@link SQLExceptionTranslator}.
|
||||
*
|
||||
* @author Dennis Melzer
|
||||
* @since 3.3.0
|
||||
* @see #DEFAULT
|
||||
* @see #of(Function)
|
||||
*/
|
||||
public interface ExceptionTranslatorExecuteListener extends ExecuteListener {
|
||||
|
||||
/**
|
||||
* Default {@link ExceptionTranslatorExecuteListener} suitable for most applications.
|
||||
*/
|
||||
ExceptionTranslatorExecuteListener DEFAULT = new DefaultExceptionTranslatorExecuteListener();
|
||||
|
||||
/**
|
||||
* Creates a new {@link ExceptionTranslatorExecuteListener} backed by an
|
||||
* {@link SQLExceptionTranslator}.
|
||||
* @param translatorFactory factory function used to create the
|
||||
* {@link SQLExceptionTranslator}
|
||||
* @return a new {@link ExceptionTranslatorExecuteListener} instance
|
||||
*/
|
||||
static ExceptionTranslatorExecuteListener of(Function<ExecuteContext, SQLExceptionTranslator> translatorFactory) {
|
||||
return new DefaultExceptionTranslatorExecuteListener(translatorFactory);
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -70,8 +70,15 @@ public class JooqAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
public DefaultExecuteListenerProvider jooqExceptionTranslatorExecuteListenerProvider() {
|
||||
return new DefaultExecuteListenerProvider(new JooqExceptionTranslator());
|
||||
public DefaultExecuteListenerProvider jooqExceptionTranslatorExecuteListenerProvider(
|
||||
ExceptionTranslatorExecuteListener exceptionTranslatorExecuteListener) {
|
||||
return new DefaultExecuteListenerProvider(exceptionTranslatorExecuteListener);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ExceptionTranslatorExecuteListener.class)
|
||||
public ExceptionTranslatorExecuteListener jooqExceptionTranslator() {
|
||||
return ExceptionTranslatorExecuteListener.DEFAULT;
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -18,80 +18,33 @@ package org.springframework.boot.autoconfigure.jooq;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jooq.ExecuteContext;
|
||||
import org.jooq.ExecuteListener;
|
||||
import org.jooq.SQLDialect;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
|
||||
import org.springframework.jdbc.support.SQLExceptionTranslator;
|
||||
import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator;
|
||||
|
||||
/**
|
||||
* Transforms {@link java.sql.SQLException} into a Spring-specific
|
||||
* {@link DataAccessException}.
|
||||
* Transforms {@link SQLException} into a Spring-specific {@link DataAccessException}.
|
||||
*
|
||||
* @author Lukas Eder
|
||||
* @author Andreas Ahlenstorf
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
* @since 1.5.10
|
||||
* @deprecated since 3.3.0 for removal in 3.5.0 in favor of
|
||||
* {@link ExceptionTranslatorExecuteListener#DEFAULT} or
|
||||
* {@link ExceptionTranslatorExecuteListener#of}
|
||||
*/
|
||||
@Deprecated(since = "3.3.0", forRemoval = true)
|
||||
public class JooqExceptionTranslator implements ExecuteListener {
|
||||
|
||||
// Based on the jOOQ-spring-example from https://github.com/jOOQ/jOOQ
|
||||
|
||||
private static final Log logger = LogFactory.getLog(JooqExceptionTranslator.class);
|
||||
private final DefaultExceptionTranslatorExecuteListener delegate = new DefaultExceptionTranslatorExecuteListener(
|
||||
LogFactory.getLog(JooqExceptionTranslator.class));
|
||||
|
||||
@Override
|
||||
public void exception(ExecuteContext context) {
|
||||
SQLExceptionTranslator translator = getTranslator(context);
|
||||
// The exception() callback is not only triggered for SQL exceptions but also for
|
||||
// "normal" exceptions. In those cases sqlException() returns null.
|
||||
SQLException exception = context.sqlException();
|
||||
while (exception != null) {
|
||||
handle(context, translator, exception);
|
||||
exception = exception.getNextException();
|
||||
}
|
||||
}
|
||||
|
||||
private SQLExceptionTranslator getTranslator(ExecuteContext context) {
|
||||
SQLDialect dialect = context.configuration().dialect();
|
||||
if (dialect != null && dialect.thirdParty() != null) {
|
||||
String dbName = dialect.thirdParty().springDbName();
|
||||
if (dbName != null) {
|
||||
return new SQLErrorCodeSQLExceptionTranslator(dbName);
|
||||
}
|
||||
}
|
||||
return new SQLStateSQLExceptionTranslator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a single exception in the chain. SQLExceptions might be nested multiple
|
||||
* levels deep. The outermost exception is usually the least interesting one ("Call
|
||||
* getNextException to see the cause."). Therefore the innermost exception is
|
||||
* propagated and all other exceptions are logged.
|
||||
* @param context the execute context
|
||||
* @param translator the exception translator
|
||||
* @param exception the exception
|
||||
*/
|
||||
private void handle(ExecuteContext context, SQLExceptionTranslator translator, SQLException exception) {
|
||||
DataAccessException translated = translate(context, translator, exception);
|
||||
if (exception.getNextException() == null) {
|
||||
if (translated != null) {
|
||||
context.exception(translated);
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.error("Execution of SQL statement failed.", (translated != null) ? translated : exception);
|
||||
}
|
||||
}
|
||||
|
||||
private DataAccessException translate(ExecuteContext context, SQLExceptionTranslator translator,
|
||||
SQLException exception) {
|
||||
return translator.translate("jOOQ", context.sql(), exception);
|
||||
this.delegate.exception(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.jooq;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jooq.Configuration;
|
||||
import org.jooq.ExecuteContext;
|
||||
import org.jooq.SQLDialect;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import org.springframework.jdbc.BadSqlGrammarException;
|
||||
import org.springframework.jdbc.support.SQLExceptionTranslator;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.assertArg;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultExceptionTranslatorExecuteListener}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DefaultExceptionTranslatorExecuteListenerTests {
|
||||
|
||||
private final ExceptionTranslatorExecuteListener listener = new DefaultExceptionTranslatorExecuteListener();
|
||||
|
||||
@Test
|
||||
void createWhenTranslatorFactoryIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new DefaultExceptionTranslatorExecuteListener(
|
||||
(Function<ExecuteContext, SQLExceptionTranslator>) null))
|
||||
.withMessage("TranslatorFactory must not be null");
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@MethodSource
|
||||
void exceptionTranslatesSqlExceptions(SQLDialect dialect, SQLException sqlException) {
|
||||
ExecuteContext context = mockContext(dialect, sqlException);
|
||||
this.listener.exception(context);
|
||||
then(context).should().exception(assertArg((ex) -> assertThat(ex).isInstanceOf(BadSqlGrammarException.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionWhenExceptionCannotBeTranslatedDoesNotCallExecuteContextException() {
|
||||
ExecuteContext context = mockContext(SQLDialect.POSTGRES, new SQLException(null, null, 123456789));
|
||||
this.listener.exception(context);
|
||||
then(context).should(never()).exception(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionWhenHasCustomTranslatorFactory() {
|
||||
SQLExceptionTranslator translator = BadSqlGrammarException::new;
|
||||
ExceptionTranslatorExecuteListener listener = new DefaultExceptionTranslatorExecuteListener(
|
||||
(context) -> translator);
|
||||
SQLException sqlException = sqlException(123);
|
||||
ExecuteContext context = mockContext(SQLDialect.DUCKDB, sqlException);
|
||||
listener.exception(context);
|
||||
then(context).should().exception(assertArg((ex) -> assertThat(ex).isInstanceOf(BadSqlGrammarException.class)));
|
||||
}
|
||||
|
||||
private ExecuteContext mockContext(SQLDialect dialect, SQLException sqlException) {
|
||||
ExecuteContext context = mock(ExecuteContext.class);
|
||||
Configuration configuration = mock(Configuration.class);
|
||||
given(context.configuration()).willReturn(configuration);
|
||||
given(configuration.dialect()).willReturn(dialect);
|
||||
given(context.sqlException()).willReturn(sqlException);
|
||||
return context;
|
||||
}
|
||||
|
||||
static Object[] exceptionTranslatesSqlExceptions() {
|
||||
return new Object[] { new Object[] { SQLDialect.DERBY, sqlException("42802") },
|
||||
new Object[] { SQLDialect.H2, sqlException(42000) },
|
||||
new Object[] { SQLDialect.HSQLDB, sqlException(-22) },
|
||||
new Object[] { SQLDialect.MARIADB, sqlException(1054) },
|
||||
new Object[] { SQLDialect.MYSQL, sqlException(1054) },
|
||||
new Object[] { SQLDialect.POSTGRES, sqlException("03000") },
|
||||
new Object[] { SQLDialect.SQLITE, sqlException("21000") } };
|
||||
}
|
||||
|
||||
private static SQLException sqlException(String sqlState) {
|
||||
return new SQLException(null, sqlState);
|
||||
}
|
||||
|
||||
private static SQLException sqlException(int vendorCode) {
|
||||
return new SQLException(null, null, vendorCode);
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -55,6 +55,7 @@ import static org.mockito.Mockito.mock;
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
* @author Dmytro Nosan
|
||||
* @author Dennis Melzer
|
||||
*/
|
||||
class JooqAutoConfigurationTests {
|
||||
|
||||
@ -180,6 +181,26 @@ class JooqAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void jooqExceptionTranslatorProviderFromConfigurationCustomizerOverridesJooqExceptionTranslatorBean() {
|
||||
this.contextRunner
|
||||
.withUserConfiguration(JooqDataSourceConfiguration.class, CustomJooqExceptionTranslatorConfiguration.class)
|
||||
.run((context) -> {
|
||||
assertThat(context.getBean(ExceptionTranslatorExecuteListener.class))
|
||||
.isInstanceOf(CustomJooqExceptionTranslator.class);
|
||||
assertThat(context.getBean(DefaultExecuteListenerProvider.class).provide())
|
||||
.isInstanceOf(CustomJooqExceptionTranslator.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void jooqWithDefaultJooqExceptionTranslator() {
|
||||
this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class).run((context) -> {
|
||||
ExceptionTranslatorExecuteListener translator = context.getBean(ExceptionTranslatorExecuteListener.class);
|
||||
assertThat(translator).isInstanceOf(DefaultExceptionTranslatorExecuteListener.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void transactionProviderFromConfigurationCustomizerOverridesTransactionProviderBean() {
|
||||
this.contextRunner
|
||||
@ -254,6 +275,16 @@ class JooqAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomJooqExceptionTranslatorConfiguration {
|
||||
|
||||
@Bean
|
||||
ExceptionTranslatorExecuteListener jooqExceptionTranslator() {
|
||||
return new CustomJooqExceptionTranslator();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomTransactionProviderFromCustomizerConfiguration {
|
||||
|
||||
@ -303,4 +334,8 @@ class JooqAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
static class CustomJooqExceptionTranslator implements ExceptionTranslatorExecuteListener {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -40,6 +40,8 @@ import static org.mockito.Mockito.never;
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@Deprecated(since = "3.3.0")
|
||||
@SuppressWarnings("removal")
|
||||
class JooqExceptionTranslatorTests {
|
||||
|
||||
private final JooqExceptionTranslator exceptionTranslator = new JooqExceptionTranslator();
|
||||
|
Loading…
Reference in New Issue
Block a user