From e63b63e2935974b9b1697b1734741058247a4843 Mon Sep 17 00:00:00 2001 From: Jean Bisutti Date: Wed, 21 Oct 2026 23:48:21 +0200 Subject: [PATCH] Fix @ProfileConnection stack trace starting with a Spring AOP frame --- ...ingBoot3ConnectionProfilingJunit5Test.java | 58 ++++++++ .../service/SpringDbConnectionProfiling.java | 61 ++++++++ .../QuickPerfSpringStackTraceTraceFilter.java | 27 +++- ...ckPerfSpringStackTraceTraceFilterTest.java | 133 ++++++++++++++++++ 4 files changed, 273 insertions(+), 6 deletions(-) create mode 100644 spring/junit5-spring-boot-3-test/src/test/java/org/quickperf/spring/springboottest/SpringBoot3ConnectionProfilingJunit5Test.java create mode 100644 spring/junit5-spring-boot-3-test/src/test/java/org/quickperf/spring/springboottest/service/SpringDbConnectionProfiling.java create mode 100644 sql/sql-annotations/src/test/java/org/quickperf/sql/connection/stack/QuickPerfSpringStackTraceTraceFilterTest.java diff --git a/spring/junit5-spring-boot-3-test/src/test/java/org/quickperf/spring/springboottest/SpringBoot3ConnectionProfilingJunit5Test.java b/spring/junit5-spring-boot-3-test/src/test/java/org/quickperf/spring/springboottest/SpringBoot3ConnectionProfilingJunit5Test.java new file mode 100644 index 00000000..2bbe2b4a --- /dev/null +++ b/spring/junit5-spring-boot-3-test/src/test/java/org/quickperf/spring/springboottest/SpringBoot3ConnectionProfilingJunit5Test.java @@ -0,0 +1,58 @@ +/* + * 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 + * + * http://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. + * + * Copyright 2019-2022 the original author or authors. + */ +package org.quickperf.spring.springboottest; + +import org.junit.jupiter.api.Test; +import org.quickperf.junit5.JUnit5Tests; +import org.quickperf.junit5.JUnit5Tests.JUnit5TestsResult; +import org.quickperf.spring.springboottest.service.SpringDbConnectionProfiling; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import static java.util.stream.Collectors.joining; +import static org.assertj.core.api.Assertions.assertThat; + +class SpringBoot3ConnectionProfilingJunit5Test { + + @Test + void should_start_the_stack_trace_of_a_data_source_call_with_the_data_source_proxy() throws IOException { + + // GIVEN + Class testClass = SpringDbConnectionProfiling.class; + JUnit5Tests jUnit5Tests = JUnit5Tests.createInstance(testClass); + + // WHEN + JUnit5TestsResult jUnit5TestsResult = jUnit5Tests.run(); + + // THEN + assertThat(jUnit5TestsResult.getNumberOfFailures()).isZero(); + + String profilingResult = readContentOf(SpringDbConnectionProfiling.DB_PROFILING_FILE_PATH); + + // Spring Boot 3.2 brings Spring 6.1. From Spring 5.2 to 6.1, the CGLIB proxy of the DataSource calls the + // QuickPerf interceptor through three Spring AOP elements instead of two. + assertThat(profilingResult.replaceAll("connection .* -", "connection id -")) + .startsWith("connection id - javax.sql.DataSource.getConnection()" + System.lineSeparator() + + "\tcom.zaxxer.hikari.HikariDataSource$$SpringCGLIB$$" + ); + + } + + private String readContentOf(String filePath) throws IOException { + return Files.lines(Paths.get(filePath)) + .collect(joining(System.lineSeparator())); + } + +} diff --git a/spring/junit5-spring-boot-3-test/src/test/java/org/quickperf/spring/springboottest/service/SpringDbConnectionProfiling.java b/spring/junit5-spring-boot-3-test/src/test/java/org/quickperf/spring/springboottest/service/SpringDbConnectionProfiling.java new file mode 100644 index 00000000..e0e6529c --- /dev/null +++ b/spring/junit5-spring-boot-3-test/src/test/java/org/quickperf/spring/springboottest/service/SpringDbConnectionProfiling.java @@ -0,0 +1,61 @@ +/* + * 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 + * + * http://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. + * + * Copyright 2019-2022 the original author or authors. + */ +package org.quickperf.spring.springboottest.service; + +import org.junit.jupiter.api.Test; +import org.quickperf.junit5.QuickPerfTest; +import org.quickperf.spring.springboottest.FootballApplication; +import org.quickperf.sql.annotation.ProfileConnection; +import org.quickperf.writer.WriterFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Path; +import java.nio.file.Paths; + +@QuickPerfTest +@SpringBootTest(classes = {FootballApplication.class}) +public class SpringDbConnectionProfiling { + + public static final String DB_PROFILING_FILE_PATH = findTargetPath() + File.separator + "spring-connection-profiling.txt"; + + private static String findTargetPath() { + Path targetDirectory = Paths.get("target"); + return targetDirectory.toFile().getAbsolutePath(); + } + + @Autowired + private PlayerService playerService; + + @ProfileConnection(displayStackTrace = true + , writerFactory = FileWriterBuilder.class + ) + @Test + void should_find_all_players_with_team_name() { + playerService.findPlayersWithTeamName(); + } + + public static class FileWriterBuilder implements WriterFactory { + + @Override + public Writer buildWriter() throws IOException { + return new FileWriter(DB_PROFILING_FILE_PATH); + } + + } + +} diff --git a/sql/sql-annotations/src/main/java/org/quickperf/sql/connection/stack/QuickPerfSpringStackTraceTraceFilter.java b/sql/sql-annotations/src/main/java/org/quickperf/sql/connection/stack/QuickPerfSpringStackTraceTraceFilter.java index b84af345..54093f7a 100644 --- a/sql/sql-annotations/src/main/java/org/quickperf/sql/connection/stack/QuickPerfSpringStackTraceTraceFilter.java +++ b/sql/sql-annotations/src/main/java/org/quickperf/sql/connection/stack/QuickPerfSpringStackTraceTraceFilter.java @@ -24,17 +24,32 @@ public PositionsFiltering filter(StackTraceElement[] stackElements) { } private int extractSpringFirstPos(StackTraceElement[] stackElements) { - int firstPos = 0; for (int i = 0, elementsLength = stackElements.length; i < elementsLength; i++) { StackTraceElement element = stackElements[i]; if (element.toString().startsWith("org.quickperf.spring.sql.QuickPerfProxyBeanPostProcessor$ProxyDataSourceInterceptor.invoke")) { - //org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) - //org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) - firstPos = i + 3; - break; + return skipSpringAopElements(stackElements, i + 1); } } - return firstPos; + return 0; + } + + // The Spring AOP elements are skipped by package because their number depends on the Spring version. + // CGLIB proxy, Spring 3.2 to 5.1 and Spring 6.2+: + //org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) + //org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) + // CGLIB proxy, Spring 5.2 to 6.1: + //org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) + //org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768) + //org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:720) + // JDK dynamic proxy: + //org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) + //org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223) + private int skipSpringAopElements(StackTraceElement[] stackElements, int fromPos) { + int pos = fromPos; + while (pos < stackElements.length && stackElements[pos].getClassName().startsWith("org.springframework.aop.")) { + pos++; + } + return pos; } } diff --git a/sql/sql-annotations/src/test/java/org/quickperf/sql/connection/stack/QuickPerfSpringStackTraceTraceFilterTest.java b/sql/sql-annotations/src/test/java/org/quickperf/sql/connection/stack/QuickPerfSpringStackTraceTraceFilterTest.java new file mode 100644 index 00000000..9b5a9c39 --- /dev/null +++ b/sql/sql-annotations/src/test/java/org/quickperf/sql/connection/stack/QuickPerfSpringStackTraceTraceFilterTest.java @@ -0,0 +1,133 @@ +/* + * 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 + * + * http://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. + * + * Copyright 2019-2022 the original author or authors. + */ +package org.quickperf.sql.connection.stack; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class QuickPerfSpringStackTraceTraceFilterTest { + + // Top of the stack trace of DataSource.getConnection() on a DataSource proxied by QuickPerfProxyBeanPostProcessor, + // captured with JDK 17 + private static final StackTraceElement[] QUICKPERF_ELEMENTS = { + new StackTraceElement("java.lang.Thread", "getStackTrace", "Thread.java", 1610), + new StackTraceElement("org.quickperf.sql.connection.ConnectionProfiler", "findCurrentStackTrace", "ConnectionProfiler.java", 72), + new StackTraceElement("org.quickperf.sql.connection.ConnectionProfiler", "printStackTrace", "ConnectionProfiler.java", 64), + new StackTraceElement("org.quickperf.sql.connection.ConnectionProfiler", "profile", "ConnectionProfiler.java", 51), + new StackTraceElement("org.quickperf.sql.connection.ConnectionEventsProfiler", "theDatasourceGetsTheConnection", "ConnectionEventsProfiler.java", 51), + new StackTraceElement("org.quickperf.sql.connection.QuickPerfDatabaseConnection", "theDatasourceGetsTheConnection", "QuickPerfDatabaseConnection.java", 40), + new StackTraceElement("org.quickperf.sql.config.library.QuickPerfProxyDataSource", "getConnection", "QuickPerfProxyDataSource.java", 27), + new StackTraceElement("jdk.internal.reflect.NativeMethodAccessorImpl", "invoke0", "NativeMethodAccessorImpl.java", -2), + new StackTraceElement("jdk.internal.reflect.NativeMethodAccessorImpl", "invoke", "NativeMethodAccessorImpl.java", 77), + new StackTraceElement("jdk.internal.reflect.DelegatingMethodAccessorImpl", "invoke", "DelegatingMethodAccessorImpl.java", 43), + new StackTraceElement("java.lang.reflect.Method", "invoke", "Method.java", 568), + new StackTraceElement("org.quickperf.spring.sql.QuickPerfProxyBeanPostProcessor$ProxyDataSourceInterceptor", "invoke", "QuickPerfProxyBeanPostProcessor.java", 160) + }; + + // Callers of the DataSource proxy, through the Spring AOP proxy of a @Transactional service. + // These Spring AOP elements have to stay in the stack trace. + private static final StackTraceElement[] CALLER_ELEMENTS = { + new StackTraceElement("org.example.BookRepository", "findAll", "BookRepository.java", 31), + new StackTraceElement("org.springframework.transaction.interceptor.TransactionInterceptor", "invoke", "TransactionInterceptor.java", 119), + new StackTraceElement("org.springframework.aop.framework.ReflectiveMethodInvocation", "proceed", "ReflectiveMethodInvocation.java", 186), + new StackTraceElement("org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor", "intercept", "CglibAopProxy.java", 688), + new StackTraceElement("org.example.BookService$$EnhancerBySpringCGLIB$$9f2c3e1a", "findBooks", "", -1) + }; + + @Test public void + should_start_the_stack_trace_with_the_data_source_proxy_if_two_spring_aop_elements_call_the_interceptor() { + + // GIVEN + // CGLIB proxy, Spring 3.2 to 5.1 and Spring 6.2+ + StackTraceElement[] springAopElements = { + new StackTraceElement("org.springframework.aop.framework.ReflectiveMethodInvocation", "proceed", "ReflectiveMethodInvocation.java", 186), + new StackTraceElement("org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor", "intercept", "CglibAopProxy.java", 688) + }; + StackTraceElement dataSourceProxyElement + = new StackTraceElement("com.zaxxer.hikari.HikariDataSource$$EnhancerBySpringCGLIB$$2e424f09", "getConnection", "", -1); + StackTraceElement[] stackTrace = buildGetConnectionStackTrace(springAopElements, dataSourceProxyElement); + + // WHEN + StackTraceElement[] formattedStackTrace = buildConfigFilteringStackTrace().format(stackTrace); + + // THEN + assertThat(formattedStackTrace).containsExactly(concat(new StackTraceElement[] {dataSourceProxyElement}, CALLER_ELEMENTS)); + + } + + @Test public void + should_start_the_stack_trace_with_the_data_source_proxy_if_three_spring_aop_elements_call_the_interceptor() { + + // GIVEN + // CGLIB proxy, Spring 5.2 to 6.1 + StackTraceElement[] springAopElements = { + new StackTraceElement("org.springframework.aop.framework.ReflectiveMethodInvocation", "proceed", "ReflectiveMethodInvocation.java", 184), + new StackTraceElement("org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation", "proceed", "CglibAopProxy.java", 768), + new StackTraceElement("org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor", "intercept", "CglibAopProxy.java", 720) + }; + StackTraceElement dataSourceProxyElement + = new StackTraceElement("com.zaxxer.hikari.HikariDataSource$$SpringCGLIB$$0", "getConnection", "", -1); + StackTraceElement[] stackTrace = buildGetConnectionStackTrace(springAopElements, dataSourceProxyElement); + + // WHEN + StackTraceElement[] formattedStackTrace = buildConfigFilteringStackTrace().format(stackTrace); + + // THEN + assertThat(formattedStackTrace).containsExactly(concat(new StackTraceElement[] {dataSourceProxyElement}, CALLER_ELEMENTS)); + + } + + @Test public void + should_start_the_stack_trace_with_the_data_source_proxy_if_it_is_a_jdk_dynamic_proxy() { + + // GIVEN + StackTraceElement[] springAopElements = { + new StackTraceElement("org.springframework.aop.framework.ReflectiveMethodInvocation", "proceed", "ReflectiveMethodInvocation.java", 184), + new StackTraceElement("org.springframework.aop.framework.JdkDynamicAopProxy", "invoke", "JdkDynamicAopProxy.java", 223) + }; + StackTraceElement dataSourceProxyElement + = new StackTraceElement("com.sun.proxy.$Proxy118", "getConnection", null, -1); + StackTraceElement[] stackTrace = buildGetConnectionStackTrace(springAopElements, dataSourceProxyElement); + + // WHEN + StackTraceElement[] formattedStackTrace = buildConfigFilteringStackTrace().format(stackTrace); + + // THEN + assertThat(formattedStackTrace).containsExactly(concat(new StackTraceElement[] {dataSourceProxyElement}, CALLER_ELEMENTS)); + + } + + private static StackTraceElement[] buildGetConnectionStackTrace(StackTraceElement[] springAopElements, StackTraceElement dataSourceProxyElement) { + StackTraceElement[] elementsUpToDataSourceProxy = concat( concat(QUICKPERF_ELEMENTS, springAopElements) + , new StackTraceElement[] {dataSourceProxyElement}); + return concat(elementsUpToDataSourceProxy, CALLER_ELEMENTS); + } + + private static StackTraceDisplayConfig buildConfigFilteringStackTrace() { + List stackTraceFilters = Arrays.asList( QuickPerfStackTraceTraceFilter.INSTANCE + , QuickPerfSpringStackTraceTraceFilter.INSTANCE); + return StackTraceDisplayConfig.of(StackTrace.StackDepth.ALL, stackTraceFilters); + } + + private static StackTraceElement[] concat(StackTraceElement[] firstElements, StackTraceElement[] lastElements) { + List elements = new ArrayList<>(Arrays.asList(firstElements)); + elements.addAll(Arrays.asList(lastElements)); + return elements.toArray(new StackTraceElement[0]); + } + +}