Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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()));
}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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", "<generated>", -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", "<generated>", -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", "<generated>", -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<StackTraceFilter> stackTraceFilters = Arrays.<StackTraceFilter>asList( QuickPerfStackTraceTraceFilter.INSTANCE
, QuickPerfSpringStackTraceTraceFilter.INSTANCE);
return StackTraceDisplayConfig.of(StackTrace.StackDepth.ALL, stackTraceFilters);
}

private static StackTraceElement[] concat(StackTraceElement[] firstElements, StackTraceElement[] lastElements) {
List<StackTraceElement> elements = new ArrayList<>(Arrays.asList(firstElements));
elements.addAll(Arrays.asList(lastElements));
return elements.toArray(new StackTraceElement[0]);
}

}
Loading