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:
Phillip Webb 2024-01-22 15:54:10 -08:00
commit cf8e06a7a4
7 changed files with 355 additions and 61 deletions

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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 {
}
}

View File

@ -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();