├── .github
├── FUNDING.yml
└── workflows
│ ├── build.yml
│ └── release.yml
├── .gitignore
├── .gitpod.Dockerfile
├── .gitpod.yml
├── .mvn
└── wrapper
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── LICENSE
├── README.md
├── docs
└── images
│ ├── flamechart.png
│ └── table.png
├── example
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── startupdemo
│ │ │ └── StartupDemoApplication.java
│ └── resources
│ │ └── application.properties
│ └── test
│ └── java
│ └── com
│ └── example
│ └── startupdemo
│ ├── StartupDemoApplication2Tests.java
│ └── StartupDemoApplicationTests.java
├── mvnw
├── mvnw.cmd
├── pom.xml
└── startup-report
├── pom.xml
└── src
├── main
├── java
│ └── com
│ │ └── maciejwalkowiak
│ │ └── spring
│ │ └── boot
│ │ └── startup
│ │ ├── Event.java
│ │ ├── ReportRenderer.java
│ │ ├── StartupEventsAutoConfiguration.java
│ │ ├── StartupEventsController.java
│ │ ├── StartupEventsCustomizer.java
│ │ ├── StartupEventsCustomizerFactory.java
│ │ ├── StartupEventsInitializer.java
│ │ ├── TagsResolver.java
│ │ └── TimelineFactory.java
└── resources
│ ├── META-INF
│ ├── additional-spring-configuration-metadata.json
│ ├── spring.factories
│ └── spring
│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ └── com
│ └── maciejwalkowiak
│ └── spring
│ └── boot
│ └── startup
│ └── startup-analysis.html
└── test
└── java
└── com
├── demo
├── App.java
└── FooService.java
└── maciejwalkowiak
└── spring
└── boot
└── startup
├── ReportRendererTests.java
├── TagsResolverTest.java
└── TimelineFactoryTest.java
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: maciejwalkowiak
2 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | build:
14 | name: "Build"
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Cache local Maven repository
19 | uses: actions/cache@v2
20 | with:
21 | path: ~/.m2/repository
22 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
23 | restore-keys: ${{ runner.os }}-maven-
24 | - name: Set up Java
25 | uses: actions/setup-java@v3
26 | with:
27 | java-version: '8'
28 | distribution: 'adopt'
29 | - name: Build with Maven
30 | run: ./mvnw verify
31 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish package to the Maven Central Repository
2 | on:
3 | push:
4 | tags:
5 | - v*
6 | jobs:
7 | publish:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v3
11 | - name: Set up Java
12 | uses: actions/setup-java@v3
13 | with:
14 | java-version: '8'
15 | distribution: 'adopt'
16 | - name: Publish package
17 | env:
18 | JRELEASER_NEXUS2_USERNAME: ${{ secrets.JRELEASER_NEXUS2_USERNAME }}
19 | JRELEASER_NEXUS2_PASSWORD: ${{ secrets.JRELEASER_NEXUS2_PASSWORD }}
20 | JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
21 | JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
22 | JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }}
23 | JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24 | run: ./mvnw -Prelease deploy jreleaser:deploy -DaltDeploymentRepository=local::default::file:./target/staging-deploy
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/.gitpod.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM gitpod/workspace-full
2 |
3 | USER gitpod
4 |
5 | RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && \
6 | sdk install java 8.0.362-tem && \
7 | sdk default java 8.0.362-tem"
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | image:
2 | file: .gitpod.Dockerfile
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maciejwalkowiak/spring-boot-startup-report/b3eb93170bec1424c1cc028db48fc94204dcc181/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip
18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Maciej Walkowiak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spring Boot Startup Report
2 |
3 | **Spring Boot Startup Report** library generates an interactive Spring Boot application startup report that lets you understand
4 | what contributes to the application startup time and perhaps helps to optimize it.
5 |
6 | [](https://gitpod.io/#https://github.com/maciejwalkowiak/spring-boot-startup-report)
7 |
8 |
9 | ## 🤩 Highlights
10 |
11 | - startup report available in runtime as an interactive HTML page
12 | - generating startup reports in integration tests
13 | - flame chart
14 | - search by class or an annotation
15 |
16 | Report table lets you dig into the details of beans instantiation:
17 |
18 | 
19 |
20 | Flame graphs is a more visual representation of the data from the table:
21 |
22 | 
23 |
24 | ## ⚠️ Minimum Requirements
25 |
26 | To use the library your project must use following versions
27 |
28 | - Java 8
29 | - Spring Boot 2.7+, 3.0+
30 |
31 | ## ✨ How to use
32 |
33 | > **Note**
34 | > The report generation depends on Jackson present on the classpath. If you already have `spring-boot-starter-web` or `spring-boot-starter-json` or any other dependency that brings Jackson - there is nothing to worry about, otherwise make sure to add this to your dependency list:
35 | > ```
36 | >
37 | > com.fasterxml.jackson.core
38 | > jackson-databind
39 | >
40 | > ```
41 |
42 | 1. Add the dependency to `spring-boot-startup-report`:
43 |
44 | ```xml
45 |
46 | com.maciejwalkowiak.spring
47 | spring-boot-startup-report
48 | 0.2.0
49 | true
50 |
51 | ```
52 |
53 | 2. Run application.
54 |
55 | 3. Assuming application runs on port `8080`, go to `http://localhost:8080/startup-report`
56 |
57 | > **Note**
58 | > This library has dependencies to `org.springframework:spring-test` and `org.springframework.boot:spring-boot-test` so most likely you don't want to include it to your production build and therefore we use it as an optional dependency.
59 |
60 | ## Using with integration tests
61 |
62 | When library is on the classpath, it also automatically generates startup reports for each application context started during running integration tests (tests annotated with `@SpringBootTest`).
63 | You'll find them in `target/startup-reports` for Maven projects and in `build/startup-reports` for Gradle.
64 |
65 | For integration tests that do not use `@SpringBootTest` but `@WebMvcTest`, `@DataJpaTest` or any other test slice, add `@Import(StartupEventsAutoConfiguration.class)` on the top of the test class to enable generating report.
66 |
67 | ```java
68 | @Import(StartupEventsAutoConfiguration.class)
69 | @WebMvcTest(OwnerController.class)
70 | public class OwnerControllerTests {
71 | ...
72 | }
73 | ```
74 |
75 | If you need only to generate reports for tests, but do not need to have the report available in runtime under an endpoint, you can declare the dependency with a `test` scope:
76 |
77 | ```xml
78 |
79 | com.maciejwalkowiak.spring
80 | spring-boot-startup-report
81 | 0.2.0
82 | test
83 |
84 | ```
85 |
86 | Sounds good? Consider [❤️ Sponsoring](https://github.com/sponsors/maciejwalkowiak) the project! Thank you!
87 |
--------------------------------------------------------------------------------
/docs/images/flamechart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maciejwalkowiak/spring-boot-startup-report/b3eb93170bec1424c1cc028db48fc94204dcc181/docs/images/flamechart.png
--------------------------------------------------------------------------------
/docs/images/table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maciejwalkowiak/spring-boot-startup-report/b3eb93170bec1424c1cc028db48fc94204dcc181/docs/images/table.png
--------------------------------------------------------------------------------
/example/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 |
8 | com.maciejwalkowiak.spring
9 | spring-boot-startup-report-parent
10 | 0.2.1-SNAPSHOT
11 | ../pom.xml
12 |
13 |
14 | spring-boot-startup-report-example
15 | ${project.parent.version}
16 |
17 |
18 |
19 | org.springframework.boot
20 | spring-boot-starter-web
21 |
22 |
23 | com.maciejwalkowiak.spring
24 | spring-boot-startup-report
25 | ${project.version}
26 |
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-test
31 | test
32 |
33 |
34 |
35 |
36 |
37 |
38 | org.springframework.boot
39 | spring-boot-maven-plugin
40 |
41 |
42 | org.apache.maven.plugins
43 | maven-deploy-plugin
44 |
45 | true
46 |
47 |
48 |
49 | org.jreleaser
50 | jreleaser-maven-plugin
51 |
52 | true
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/example/src/main/java/com/example/startupdemo/StartupDemoApplication.java:
--------------------------------------------------------------------------------
1 | package com.example.startupdemo;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.stereotype.Service;
6 |
7 | @SpringBootApplication
8 | public class StartupDemoApplication {
9 |
10 | public static void main(String[] args) {
11 | SpringApplication.run(StartupDemoApplication.class, args);
12 | }
13 |
14 | }
15 |
16 | @Service
17 | class FooService {
18 |
19 | }
--------------------------------------------------------------------------------
/example/src/main/resources/application.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maciejwalkowiak/spring-boot-startup-report/b3eb93170bec1424c1cc028db48fc94204dcc181/example/src/main/resources/application.properties
--------------------------------------------------------------------------------
/example/src/test/java/com/example/startupdemo/StartupDemoApplication2Tests.java:
--------------------------------------------------------------------------------
1 | package com.example.startupdemo;
2 |
3 | import java.io.IOException;
4 |
5 | import org.junit.jupiter.api.Test;
6 |
7 | import org.springframework.boot.test.context.SpringBootTest;
8 |
9 | @SpringBootTest
10 | public class StartupDemoApplication2Tests {
11 |
12 | @Test
13 | void contextLoads2() throws IOException {
14 | System.out.println("xx");
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/example/src/test/java/com/example/startupdemo/StartupDemoApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.example.startupdemo;
2 |
3 | import java.io.IOException;
4 |
5 | import org.junit.jupiter.api.Test;
6 |
7 | import org.springframework.boot.test.context.SpringBootTest;
8 |
9 | @SpringBootTest(properties = "xx=foo")
10 | class StartupDemoApplicationTests {
11 |
12 | @Test
13 | void contextLoads() throws IOException {
14 |
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # https://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /usr/local/etc/mavenrc ] ; then
40 | . /usr/local/etc/mavenrc
41 | fi
42 |
43 | if [ -f /etc/mavenrc ] ; then
44 | . /etc/mavenrc
45 | fi
46 |
47 | if [ -f "$HOME/.mavenrc" ] ; then
48 | . "$HOME/.mavenrc"
49 | fi
50 |
51 | fi
52 |
53 | # OS specific support. $var _must_ be set to either true or false.
54 | cygwin=false;
55 | darwin=false;
56 | mingw=false
57 | case "`uname`" in
58 | CYGWIN*) cygwin=true ;;
59 | MINGW*) mingw=true;;
60 | Darwin*) darwin=true
61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
63 | if [ -z "$JAVA_HOME" ]; then
64 | if [ -x "/usr/libexec/java_home" ]; then
65 | export JAVA_HOME="`/usr/libexec/java_home`"
66 | else
67 | export JAVA_HOME="/Library/Java/Home"
68 | fi
69 | fi
70 | ;;
71 | esac
72 |
73 | if [ -z "$JAVA_HOME" ] ; then
74 | if [ -r /etc/gentoo-release ] ; then
75 | JAVA_HOME=`java-config --jre-home`
76 | fi
77 | fi
78 |
79 | if [ -z "$M2_HOME" ] ; then
80 | ## resolve links - $0 may be a link to maven's home
81 | PRG="$0"
82 |
83 | # need this for relative symlinks
84 | while [ -h "$PRG" ] ; do
85 | ls=`ls -ld "$PRG"`
86 | link=`expr "$ls" : '.*-> \(.*\)$'`
87 | if expr "$link" : '/.*' > /dev/null; then
88 | PRG="$link"
89 | else
90 | PRG="`dirname "$PRG"`/$link"
91 | fi
92 | done
93 |
94 | saveddir=`pwd`
95 |
96 | M2_HOME=`dirname "$PRG"`/..
97 |
98 | # make it fully qualified
99 | M2_HOME=`cd "$M2_HOME" && pwd`
100 |
101 | cd "$saveddir"
102 | # echo Using m2 at $M2_HOME
103 | fi
104 |
105 | # For Cygwin, ensure paths are in UNIX format before anything is touched
106 | if $cygwin ; then
107 | [ -n "$M2_HOME" ] &&
108 | M2_HOME=`cygpath --unix "$M2_HOME"`
109 | [ -n "$JAVA_HOME" ] &&
110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
111 | [ -n "$CLASSPATH" ] &&
112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
113 | fi
114 |
115 | # For Mingw, ensure paths are in UNIX format before anything is touched
116 | if $mingw ; then
117 | [ -n "$M2_HOME" ] &&
118 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
119 | [ -n "$JAVA_HOME" ] &&
120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
121 | fi
122 |
123 | if [ -z "$JAVA_HOME" ]; then
124 | javaExecutable="`which javac`"
125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
126 | # readlink(1) is not available as standard on Solaris 10.
127 | readLink=`which readlink`
128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
129 | if $darwin ; then
130 | javaHome="`dirname \"$javaExecutable\"`"
131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
132 | else
133 | javaExecutable="`readlink -f \"$javaExecutable\"`"
134 | fi
135 | javaHome="`dirname \"$javaExecutable\"`"
136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
137 | JAVA_HOME="$javaHome"
138 | export JAVA_HOME
139 | fi
140 | fi
141 | fi
142 |
143 | if [ -z "$JAVACMD" ] ; then
144 | if [ -n "$JAVA_HOME" ] ; then
145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
146 | # IBM's JDK on AIX uses strange locations for the executables
147 | JAVACMD="$JAVA_HOME/jre/sh/java"
148 | else
149 | JAVACMD="$JAVA_HOME/bin/java"
150 | fi
151 | else
152 | JAVACMD="`\\unset -f command; \\command -v java`"
153 | fi
154 | fi
155 |
156 | if [ ! -x "$JAVACMD" ] ; then
157 | echo "Error: JAVA_HOME is not defined correctly." >&2
158 | echo " We cannot execute $JAVACMD" >&2
159 | exit 1
160 | fi
161 |
162 | if [ -z "$JAVA_HOME" ] ; then
163 | echo "Warning: JAVA_HOME environment variable is not set."
164 | fi
165 |
166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
167 |
168 | # traverses directory structure from process work directory to filesystem root
169 | # first directory with .mvn subdirectory is considered project base directory
170 | find_maven_basedir() {
171 |
172 | if [ -z "$1" ]
173 | then
174 | echo "Path not specified to find_maven_basedir"
175 | return 1
176 | fi
177 |
178 | basedir="$1"
179 | wdir="$1"
180 | while [ "$wdir" != '/' ] ; do
181 | if [ -d "$wdir"/.mvn ] ; then
182 | basedir=$wdir
183 | break
184 | fi
185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
186 | if [ -d "${wdir}" ]; then
187 | wdir=`cd "$wdir/.."; pwd`
188 | fi
189 | # end of workaround
190 | done
191 | echo "${basedir}"
192 | }
193 |
194 | # concatenates all lines of a file
195 | concat_lines() {
196 | if [ -f "$1" ]; then
197 | echo "$(tr -s '\n' ' ' < "$1")"
198 | fi
199 | }
200 |
201 | BASE_DIR=`find_maven_basedir "$(pwd)"`
202 | if [ -z "$BASE_DIR" ]; then
203 | exit 1;
204 | fi
205 |
206 | ##########################################################################################
207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
208 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
209 | ##########################################################################################
210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
211 | if [ "$MVNW_VERBOSE" = true ]; then
212 | echo "Found .mvn/wrapper/maven-wrapper.jar"
213 | fi
214 | else
215 | if [ "$MVNW_VERBOSE" = true ]; then
216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
217 | fi
218 | if [ -n "$MVNW_REPOURL" ]; then
219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
220 | else
221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
222 | fi
223 | while IFS="=" read key value; do
224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
225 | esac
226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
227 | if [ "$MVNW_VERBOSE" = true ]; then
228 | echo "Downloading from: $jarUrl"
229 | fi
230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
231 | if $cygwin; then
232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
233 | fi
234 |
235 | if command -v wget > /dev/null; then
236 | if [ "$MVNW_VERBOSE" = true ]; then
237 | echo "Found wget ... using wget"
238 | fi
239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
241 | else
242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
243 | fi
244 | elif command -v curl > /dev/null; then
245 | if [ "$MVNW_VERBOSE" = true ]; then
246 | echo "Found curl ... using curl"
247 | fi
248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
249 | curl -o "$wrapperJarPath" "$jarUrl" -f
250 | else
251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
252 | fi
253 |
254 | else
255 | if [ "$MVNW_VERBOSE" = true ]; then
256 | echo "Falling back to using Java to download"
257 | fi
258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
259 | # For Cygwin, switch paths to Windows format before running javac
260 | if $cygwin; then
261 | javaClass=`cygpath --path --windows "$javaClass"`
262 | fi
263 | if [ -e "$javaClass" ]; then
264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
265 | if [ "$MVNW_VERBOSE" = true ]; then
266 | echo " - Compiling MavenWrapperDownloader.java ..."
267 | fi
268 | # Compiling the Java class
269 | ("$JAVA_HOME/bin/javac" "$javaClass")
270 | fi
271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
272 | # Running the downloader
273 | if [ "$MVNW_VERBOSE" = true ]; then
274 | echo " - Running MavenWrapperDownloader.java ..."
275 | fi
276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
277 | fi
278 | fi
279 | fi
280 | fi
281 | ##########################################################################################
282 | # End of extension
283 | ##########################################################################################
284 |
285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
286 | if [ "$MVNW_VERBOSE" = true ]; then
287 | echo $MAVEN_PROJECTBASEDIR
288 | fi
289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
290 |
291 | # For Cygwin, switch paths to Windows format before running java
292 | if $cygwin; then
293 | [ -n "$M2_HOME" ] &&
294 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
295 | [ -n "$JAVA_HOME" ] &&
296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
297 | [ -n "$CLASSPATH" ] &&
298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
299 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
301 | fi
302 |
303 | # Provide a "standardized" way to retrieve the CLI args that will
304 | # work with both Windows and non-Windows executions.
305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
306 | export MAVEN_CMD_LINE_ARGS
307 |
308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
309 |
310 | exec "$JAVACMD" \
311 | $MAVEN_OPTS \
312 | $MAVEN_DEBUG_OPTS \
313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
314 | "-Dmaven.home=${M2_HOME}" \
315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
317 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
124 |
125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
127 | )
128 |
129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
131 | if exist %WRAPPER_JAR% (
132 | if "%MVNW_VERBOSE%" == "true" (
133 | echo Found %WRAPPER_JAR%
134 | )
135 | ) else (
136 | if not "%MVNW_REPOURL%" == "" (
137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
138 | )
139 | if "%MVNW_VERBOSE%" == "true" (
140 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
141 | echo Downloading from: %DOWNLOAD_URL%
142 | )
143 |
144 | powershell -Command "&{"^
145 | "$webclient = new-object System.Net.WebClient;"^
146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
148 | "}"^
149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
150 | "}"
151 | if "%MVNW_VERBOSE%" == "true" (
152 | echo Finished downloading %WRAPPER_JAR%
153 | )
154 | )
155 | @REM End of extension
156 |
157 | @REM Provide a "standardized" way to retrieve the CLI args that will
158 | @REM work with both Windows and non-Windows executions.
159 | set MAVEN_CMD_LINE_ARGS=%*
160 |
161 | %MAVEN_JAVA_EXE% ^
162 | %JVM_CONFIG_MAVEN_PROPS% ^
163 | %MAVEN_OPTS% ^
164 | %MAVEN_DEBUG_OPTS% ^
165 | -classpath %WRAPPER_JAR% ^
166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
168 | if ERRORLEVEL 1 goto error
169 | goto end
170 |
171 | :error
172 | set ERROR_CODE=1
173 |
174 | :end
175 | @endlocal & set ERROR_CODE=%ERROR_CODE%
176 |
177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
181 | :skipRcPost
182 |
183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause
185 |
186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
187 |
188 | cmd /C exit /B %ERROR_CODE%
189 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.7.10
9 |
10 |
11 | com.maciejwalkowiak.spring
12 | spring-boot-startup-report-parent
13 | 0.2.1-SNAPSHOT
14 | Spring Boot Startup Report Parent
15 | Spring Boot Startup Report Parent
16 | https://github.com/maciejwalkowiak/spring-boot-startup-report/
17 | 2023
18 | pom
19 |
20 |
21 | 8
22 | 2.7.10
23 | 5.9.2
24 | ${java.version}
25 | ${java.version}
26 | UTF-8
27 |
28 |
29 |
30 | startup-report
31 | example
32 |
33 |
34 |
35 |
36 |
37 | org.springframework.boot
38 | spring-boot-dependencies
39 | ${spring-boot.version}
40 | pom
41 | import
42 |
43 |
44 |
45 | org.junit
46 | junit-bom
47 | ${junit.version}
48 | pom
49 | import
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | org.apache.maven.plugins
59 | maven-surefire-plugin
60 | 3.0.0
61 |
62 |
63 | org.apache.maven.plugins
64 | maven-javadoc-plugin
65 | 3.5.0
66 |
67 |
68 | org.apache.maven.plugins
69 | maven-source-plugin
70 | 3.2.1
71 |
72 |
73 | org.jreleaser
74 | jreleaser-maven-plugin
75 | 1.5.1
76 |
77 |
78 | org.springframework.boot
79 | spring-boot-maven-plugin
80 | ${spring-boot.version}
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | release
89 |
90 |
91 |
92 | org.apache.maven.plugins
93 | maven-javadoc-plugin
94 |
95 |
96 | attach-javadoc
97 |
98 | jar
99 |
100 |
101 |
102 |
103 |
104 | org.apache.maven.plugins
105 | maven-source-plugin
106 |
107 |
108 | attach-source
109 |
110 | jar
111 |
112 |
113 |
114 |
115 |
116 | org.jreleaser
117 | jreleaser-maven-plugin
118 |
119 |
120 |
121 | ALWAYS
122 | true
123 |
124 |
125 |
126 |
127 |
128 | ALWAYS
129 | https://s01.oss.sonatype.org/service/local;
130 | false
131 | false
132 | target/staging-deploy
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | https://github.com/maciejwalkowiak/spring-boot-startup-report
147 | scm:git:git://github.com/maciejwalkowiak/spring-boot-startup-report.git
148 |
149 | scm:git:ssh://git@github.com/maciejwalkowiak/spring-boot-startup-report.git
150 |
151 | HEAD
152 |
153 |
154 |
155 |
156 | MIT License
157 | http://www.opensource.org/licenses/mit-license.php
158 | repo
159 |
160 |
161 |
162 |
163 |
164 | maciejwalkowiak
165 | Maciej Walkowiak
166 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/startup-report/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | com.maciejwalkowiak.spring
7 | spring-boot-startup-report-parent
8 | 0.2.1-SNAPSHOT
9 | ../pom.xml
10 |
11 | spring-boot-startup-report
12 | 0.2.1-SNAPSHOT
13 | Spring Boot Startup Report
14 | Spring Boot Startup Report
15 | https://github.com/maciejwalkowiak/spring-boot-startup-report/
16 |
17 |
18 |
19 | org.springframework
20 | spring-test
21 | true
22 |
23 |
24 | org.springframework.boot
25 | spring-boot-configuration-processor
26 | true
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-web
31 | true
32 |
33 |
34 |
35 | org.springframework.boot
36 | spring-boot-starter-test
37 | test
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/startup-report/src/main/java/com/maciejwalkowiak/spring/boot/startup/Event.java:
--------------------------------------------------------------------------------
1 | package com.maciejwalkowiak.spring.boot.startup;
2 |
3 | import java.util.Collections;
4 | import java.util.List;
5 | import java.util.Map;
6 | import java.util.stream.Collectors;
7 |
8 | import org.springframework.boot.context.metrics.buffering.StartupTimeline;
9 | import org.springframework.boot.context.metrics.buffering.StartupTimeline.TimelineEvent;
10 |
11 | class Event {
12 | private final Long id;
13 | private final Long parentId;
14 | private final String name;
15 | private final long value;
16 | private final long actualDuration;
17 | private final Map tags;
18 | private final List children;
19 |
20 | static Event create(StartupTimeline.TimelineEvent timelineEvent, List allEvents, TagsResolver tagsResolver) {
21 | List children = findChildren(timelineEvent.getStartupStep().getId(), allEvents);;
22 |
23 | List eventChildren = children == null ? Collections.emptyList() : children.stream().map(c -> create(c, allEvents, tagsResolver)).collect(Collectors.toList());
24 |
25 | return new Event(eventChildren,
26 | timelineEvent.getStartupStep().getId(),
27 | timelineEvent.getStartupStep().getParentId(),
28 | timelineEvent.getStartupStep().getName(),
29 | timelineEvent.getDuration().toMillis(),
30 | timelineEvent.getDuration().toMillis() - eventChildren.stream().map(Event::getValue).reduce(0L, Long::sum),
31 | tagsResolver.resolveTags(timelineEvent)
32 | );
33 | }
34 |
35 | private static List findChildren(Long parentId, List allEvents) {
36 | return allEvents.stream().filter(it -> parentId.equals(it.getStartupStep().getParentId())).collect(Collectors.toList());
37 | }
38 |
39 | public Event(List children, Long id, Long parentId, String name, long value, long actualDuration,
40 | Map tags) {
41 | this.children = children;
42 | this.id = id;
43 | this.parentId = parentId;
44 | this.name = name;
45 | this.value = value;
46 | this.actualDuration = actualDuration;
47 | this.tags = tags;
48 | }
49 |
50 | public String getName() {
51 | return name;
52 | }
53 |
54 | public long getValue() {
55 | return value;
56 | }
57 |
58 | public Long getId() {
59 | return id;
60 | }
61 |
62 | public Long getParentId() {
63 | return parentId;
64 | }
65 |
66 | public List getChildren() {
67 | return children;
68 | }
69 |
70 | public Map getTags() {
71 | return tags;
72 | }
73 |
74 | public long getActualDuration() {
75 | return actualDuration;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/startup-report/src/main/java/com/maciejwalkowiak/spring/boot/startup/ReportRenderer.java:
--------------------------------------------------------------------------------
1 | package com.maciejwalkowiak.spring.boot.startup;
2 |
3 | import java.io.IOException;
4 | import java.nio.charset.StandardCharsets;
5 | import java.util.List;
6 |
7 | import com.fasterxml.jackson.core.JsonProcessingException;
8 | import com.fasterxml.jackson.databind.ObjectMapper;
9 |
10 | import org.springframework.core.io.Resource;
11 | import org.springframework.core.io.ResourceLoader;
12 | import org.springframework.util.StreamUtils;
13 |
14 | /**
15 | * Renders the startup event report to an string containing complete HTML page.
16 | *
17 | * @author Maciej Walkowiak
18 | */
19 | class ReportRenderer {
20 | private final TimelineFactory timelineFactory;
21 | private final ResourceLoader resourceLoader;
22 | private final ObjectMapper mapper = new ObjectMapper();
23 |
24 | public ReportRenderer(TimelineFactory timelineFactory, ResourceLoader resourceLoader) {
25 | this.timelineFactory = timelineFactory;
26 | this.resourceLoader = resourceLoader;
27 | }
28 |
29 | String render() {
30 | Resource resource = resourceLoader.getResource("classpath:/com/maciejwalkowiak/spring/boot/startup/startup-analysis.html");
31 | try {
32 | String template = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
33 | return template.replace("%events%", serialize(timelineFactory.getTimeline()));
34 | } catch (IOException e) {
35 | throw new RuntimeException(e);
36 | }
37 | }
38 |
39 | public String serialize(List events) {
40 | try {
41 | return mapper.writeValueAsString(events);
42 | } catch (JsonProcessingException e) {
43 | throw new RuntimeException(e);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/startup-report/src/main/java/com/maciejwalkowiak/spring/boot/startup/StartupEventsAutoConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.maciejwalkowiak.spring.boot.startup;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 |
5 | import org.springframework.boot.autoconfigure.AutoConfiguration;
6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
9 | import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
10 | import org.springframework.context.annotation.Bean;
11 | import org.springframework.core.io.ResourceLoader;
12 |
13 | @AutoConfiguration
14 | @ConditionalOnClass(ObjectMapper.class)
15 | @ConditionalOnBean({BufferingApplicationStartup.class})
16 | public class StartupEventsAutoConfiguration {
17 |
18 | @Bean
19 | TagsResolver tagsResolver() {
20 | return new TagsResolver();
21 | }
22 |
23 | @Bean
24 | TimelineFactory timelineFactory(BufferingApplicationStartup applicationStartup, TagsResolver resolver) {
25 | return new TimelineFactory(applicationStartup, resolver);
26 | }
27 |
28 | @Bean
29 | ReportRenderer reportRenderer(TimelineFactory timelineFactory, ResourceLoader resourceLoader) {
30 | return new ReportRenderer(timelineFactory, resourceLoader);
31 | }
32 |
33 | @ConditionalOnWebApplication
34 | @Bean
35 | StartupEventsController startupEventsController(ReportRenderer reportRenderer) {
36 | return new StartupEventsController(reportRenderer);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/startup-report/src/main/java/com/maciejwalkowiak/spring/boot/startup/StartupEventsController.java:
--------------------------------------------------------------------------------
1 | package com.maciejwalkowiak.spring.boot.startup;
2 |
3 | import org.springframework.http.MediaType;
4 | import org.springframework.web.bind.annotation.GetMapping;
5 | import org.springframework.web.bind.annotation.RestController;
6 |
7 | /**
8 | * Exposes startup event report over HTTP.
9 | *
10 | * @author Maciej Walkowiak
11 | */
12 | @RestController
13 | class StartupEventsController {
14 | private final ReportRenderer reportRenderer;
15 |
16 | public StartupEventsController(ReportRenderer reportRenderer) {
17 | this.reportRenderer = reportRenderer;
18 | }
19 |
20 | @GetMapping(value = "${startup-events.path:/startup-report}", produces = MediaType.TEXT_HTML_VALUE)
21 | String index() {
22 | return reportRenderer.render();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/startup-report/src/main/java/com/maciejwalkowiak/spring/boot/startup/StartupEventsCustomizer.java:
--------------------------------------------------------------------------------
1 | package com.maciejwalkowiak.spring.boot.startup;
2 |
3 | import java.io.IOException;
4 | import java.nio.file.Files;
5 | import java.nio.file.Path;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 |
9 | import org.springframework.beans.factory.NoSuchBeanDefinitionException;
10 | import org.springframework.context.ConfigurableApplicationContext;
11 | import org.springframework.context.event.ContextClosedEvent;
12 | import org.springframework.test.context.ContextCustomizer;
13 | import org.springframework.test.context.MergedContextConfiguration;
14 |
15 | import java.nio.charset.StandardCharsets;
16 | import java.nio.file.Paths;
17 |
18 | /**
19 | * Adds a listener that generates an HTML report with {@link ReportRenderer} when application context gets closed.
20 | *
21 | * @author Maciej Walkowiak
22 | */
23 | public class StartupEventsCustomizer implements ContextCustomizer {
24 | private static final Logger LOGGER = LoggerFactory.getLogger(StartupEventsCustomizer.class);
25 | private final String testName;
26 |
27 | public StartupEventsCustomizer(String name) {
28 | this.testName = name;
29 | }
30 |
31 | @Override
32 | public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
33 | context.addApplicationListener(event -> {
34 | if (event instanceof ContextClosedEvent) {
35 | try {
36 | ReportRenderer reportRenderer = context.getBean(ReportRenderer.class);
37 | try {
38 | Path buildDirectory = resolveReportDirectory();
39 | Path path = Files.write(buildDirectory.resolve(Paths.get(reportName())), reportRenderer.render().getBytes(StandardCharsets.UTF_8));
40 | LOGGER.debug("Report for test {} saved to {}", testName, path.toAbsolutePath());
41 | } catch (IOException e) {
42 | LOGGER.error("Error during rendering analysis report", e);
43 | throw new RuntimeException(e);
44 | }
45 | } catch (NoSuchBeanDefinitionException e) {
46 | LOGGER.warn("Report for test {} not generated", testName, e);
47 | }
48 | }
49 | });
50 | }
51 |
52 | private static Path resolveReportDirectory() throws IOException {
53 | Path mavenTarget = Paths.get("target");
54 | Path gradleBuild = Paths.get("build");
55 |
56 | Path buildDirectory;
57 | if (Files.exists(mavenTarget)) {
58 | buildDirectory = mavenTarget;
59 | } else if (Files.exists(gradleBuild)) {
60 | buildDirectory = gradleBuild;
61 | } else {
62 | buildDirectory = Paths.get(".");
63 | }
64 |
65 | Path reportDirectory = buildDirectory.resolve("startup-reports");
66 | if (!Files.exists(reportDirectory)) {
67 | Files.createDirectories(reportDirectory);
68 | }
69 | return reportDirectory;
70 | }
71 |
72 | private String reportName() {
73 | return "startup-report-" + testName + ".html";
74 | }
75 |
76 | @Override
77 | public boolean equals(Object obj) {
78 | return (obj != null) && (obj.getClass() == getClass());
79 | }
80 |
81 | @Override
82 | public int hashCode() {
83 | return getClass().hashCode();
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/startup-report/src/main/java/com/maciejwalkowiak/spring/boot/startup/StartupEventsCustomizerFactory.java:
--------------------------------------------------------------------------------
1 | package com.maciejwalkowiak.spring.boot.startup;
2 |
3 | import java.util.List;
4 |
5 | import org.springframework.test.context.ContextConfigurationAttributes;
6 | import org.springframework.test.context.ContextCustomizer;
7 | import org.springframework.test.context.ContextCustomizerFactory;
8 |
9 | /**
10 | * Creates a {@link StartupEventsCustomizer} to customize application context in integration tests.
11 | *
12 | * @author Maciej Walkowiak
13 | */
14 | public class StartupEventsCustomizerFactory implements ContextCustomizerFactory {
15 | @Override public ContextCustomizer createContextCustomizer(Class> testClass,
16 | List configAttributes) {
17 | return new StartupEventsCustomizer(testClass.getName());
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/startup-report/src/main/java/com/maciejwalkowiak/spring/boot/startup/StartupEventsInitializer.java:
--------------------------------------------------------------------------------
1 |
2 | package com.maciejwalkowiak.spring.boot.startup;
3 |
4 | import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
5 | import org.springframework.context.ApplicationContextInitializer;
6 | import org.springframework.context.ConfigurableApplicationContext;
7 | import org.springframework.core.metrics.ApplicationStartup;
8 |
9 | /**
10 | * Sets the {@link ConfigurableApplicationContext#getApplicationStartup()} to a {@link BufferingApplicationStartup} to collect startup events.
11 | *
12 | * @author Maciej Walkowiak
13 | */
14 | public class StartupEventsInitializer implements ApplicationContextInitializer {
15 | @Override
16 | public void initialize(ConfigurableApplicationContext applicationContext) {
17 | if (applicationContext.getApplicationStartup() == ApplicationStartup.DEFAULT) {
18 | applicationContext.setApplicationStartup(new BufferingApplicationStartup(10_000));
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/startup-report/src/main/java/com/maciejwalkowiak/spring/boot/startup/TagsResolver.java:
--------------------------------------------------------------------------------
1 | package com.maciejwalkowiak.spring.boot.startup;
2 |
3 | import java.util.Arrays;
4 | import java.util.HashMap;
5 | import java.util.Map;
6 | import java.util.stream.Collectors;
7 | import java.util.stream.StreamSupport;
8 |
9 | import org.slf4j.Logger;
10 | import org.slf4j.LoggerFactory;
11 |
12 | import org.springframework.beans.BeansException;
13 | import org.springframework.beans.factory.NoSuchBeanDefinitionException;
14 | import org.springframework.beans.factory.support.ScopeNotActiveException;
15 | import org.springframework.boot.context.metrics.buffering.StartupTimeline;
16 | import org.springframework.context.ApplicationContext;
17 | import org.springframework.context.ApplicationContextAware;
18 | import org.springframework.core.metrics.StartupStep;
19 | import org.springframework.core.metrics.StartupStep.Tags;
20 |
21 | /**
22 | * Resolves tags from {@link StartupTimeline.TimelineEvent} tags by getting more details from the {@link ApplicationContext}.
23 | *
24 | * @author Maciej Walkowiak
25 | */
26 | class TagsResolver implements ApplicationContextAware {
27 | private static final Logger LOGGER = LoggerFactory.getLogger(TagsResolver.class);
28 | private ApplicationContext ctx;
29 |
30 | Map resolveTags(StartupTimeline.TimelineEvent timelineEvent) {
31 | Map tags = toHashMap(timelineEvent.getStartupStep().getTags());
32 |
33 | if (ctx == null) {
34 | LOGGER.warn("Something is wrong, ApplicationContext is not set");
35 | return tags;
36 | }
37 |
38 | if (tags.containsKey("beanName")) {
39 | try {
40 | Object bean = ctx.getBean(tags.get("beanName"));
41 | Class> clazz = bean.getClass();
42 | tags.put("class", clazz.getName());
43 | if (bean.getClass().getAnnotations().length > 0) {
44 | tags.put("annotations", Arrays.stream(bean.getClass().getAnnotations())
45 | .map(a -> "@" + a.annotationType().getSimpleName()).collect(Collectors.joining(",")));
46 | }
47 | } catch (NoSuchBeanDefinitionException e) {
48 | LOGGER.debug("No bean with name {} found", tags.get("beanName"), e);
49 | } catch (ScopeNotActiveException e) {
50 | LOGGER.debug("Scope is not active when fetching tags from bean: {}", tags.get("beanName"), e);
51 | }
52 |
53 | }
54 | return tags;
55 | }
56 |
57 | private static Map toHashMap(Tags tags) {
58 | return new HashMap<>(StreamSupport.stream(tags.spliterator(), false)
59 | .collect(Collectors.toMap(StartupStep.Tag::getKey, StartupStep.Tag::getValue)));
60 | }
61 |
62 | @Override
63 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
64 | this.ctx = applicationContext;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/startup-report/src/main/java/com/maciejwalkowiak/spring/boot/startup/TimelineFactory.java:
--------------------------------------------------------------------------------
1 | package com.maciejwalkowiak.spring.boot.startup;
2 |
3 | import java.util.Collections;
4 | import java.util.Comparator;
5 | import java.util.List;
6 | import java.util.stream.Collectors;
7 |
8 | import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
9 | import org.springframework.boot.context.metrics.buffering.StartupTimeline;
10 |
11 | class TimelineFactory {
12 | private final BufferingApplicationStartup applicationStartup;
13 | private final TagsResolver tagsResolver;
14 |
15 | public TimelineFactory(BufferingApplicationStartup applicationStartup, TagsResolver resolver) {
16 | this.applicationStartup = applicationStartup;
17 | this.tagsResolver = resolver;
18 | }
19 |
20 | public List getTimeline() {
21 | // sort from longest to shortest
22 | List timelineEvents = applicationStartup.getBufferedTimeline()
23 | .getEvents()
24 | .stream()
25 | .sorted(Collections.reverseOrder(
26 | Comparator.comparingLong((StartupTimeline.TimelineEvent o) -> o.getDuration().toMillis())))
27 | .collect(Collectors.toList());
28 |
29 | // create a hierarchical structure
30 | return timelineEvents.stream()
31 | .map(it -> Event.create(it, timelineEvents, tagsResolver))
32 | .sorted(Collections.reverseOrder(Comparator.comparingLong(Event::getValue)))
33 | .filter(it -> it.getParentId() == null)
34 | .collect(Collectors.toList());
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/startup-report/src/main/resources/META-INF/additional-spring-configuration-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "properties": [
3 | {
4 | "defaultValue": "/startup-report",
5 | "name": "startup-events.path",
6 | "description": "Startup Events report context path",
7 | "type": "java.lang.String"
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/startup-report/src/main/resources/META-INF/spring.factories:
--------------------------------------------------------------------------------
1 | org.springframework.context.ApplicationContextInitializer=com.maciejwalkowiak.spring.boot.startup.StartupEventsInitializer
2 | org.springframework.test.context.ContextCustomizerFactory=com.maciejwalkowiak.spring.boot.startup.StartupEventsCustomizerFactory
3 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.maciejwalkowiak.spring.boot.startup.StartupEventsAutoConfiguration
4 |
--------------------------------------------------------------------------------
/startup-report/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
--------------------------------------------------------------------------------
1 | com.maciejwalkowiak.spring.boot.startup.StartupEventsAutoConfiguration
2 |
--------------------------------------------------------------------------------
/startup-report/src/main/resources/com/maciejwalkowiak/spring/boot/startup/startup-analysis.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
9 | Spring Boot Startup Analysis Report
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
66 |
67 |
68 |
69 |
70 |
276 |
277 |
278 |
--------------------------------------------------------------------------------
/startup-report/src/test/java/com/demo/App.java:
--------------------------------------------------------------------------------
1 | package com.demo;
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication;
4 |
5 | @SpringBootApplication
6 | public class App {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/startup-report/src/test/java/com/demo/FooService.java:
--------------------------------------------------------------------------------
1 | package com.demo;
2 |
3 | import org.springframework.beans.factory.annotation.Qualifier;
4 |
5 | @Qualifier("hi")
6 | public class FooService {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/startup-report/src/test/java/com/maciejwalkowiak/spring/boot/startup/ReportRendererTests.java:
--------------------------------------------------------------------------------
1 | package com.maciejwalkowiak.spring.boot.startup;
2 |
3 | import com.demo.App;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.boot.test.context.SpringBootTest;
8 |
9 | import static org.assertj.core.api.Assertions.assertThat;
10 |
11 | @SpringBootTest(classes = App.class)
12 | class ReportRendererTests {
13 |
14 | @Autowired
15 | private ReportRenderer reportRenderer;
16 |
17 | @Test
18 | void rendersReport() {
19 | String report = reportRenderer.render();
20 | assertThat(report).contains("let data = [{");
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/startup-report/src/test/java/com/maciejwalkowiak/spring/boot/startup/TagsResolverTest.java:
--------------------------------------------------------------------------------
1 | package com.maciejwalkowiak.spring.boot.startup;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.mockito.Mockito.when;
5 |
6 | import java.util.Arrays;
7 | import java.util.Iterator;
8 | import java.util.List;
9 | import java.util.Map;
10 |
11 | import org.junit.jupiter.api.Test;
12 | import org.mockito.Answers;
13 | import org.mockito.Mockito;
14 | import org.springframework.boot.context.metrics.buffering.StartupTimeline.TimelineEvent;
15 | import org.springframework.context.annotation.AnnotationConfigApplicationContext;
16 | import org.springframework.context.annotation.Bean;
17 | import org.springframework.core.metrics.StartupStep.Tag;
18 | import org.springframework.core.metrics.StartupStep.Tags;
19 |
20 | import com.demo.FooService;
21 |
22 | public class TagsResolverTest {
23 |
24 | private final TagsResolver tagsResolver = new TagsResolver();
25 |
26 | @Test
27 | void returnsSimpleTagsWhenApplicationContextNotSet() {
28 | TimelineEvent timelineEvent = Mockito.mock(TimelineEvent.class, Answers.RETURNS_DEEP_STUBS);
29 | when(timelineEvent.getStartupStep().getTags()).thenReturn(new TagsContainer(new SimpleTag("key", "val")));
30 |
31 | Map result = tagsResolver.resolveTags(timelineEvent);
32 |
33 | assertThat(result).containsEntry("key", "val");
34 | }
35 |
36 | @Test
37 | void returnsTagWithClassName() {
38 | TimelineEvent timelineEvent = Mockito.mock(TimelineEvent.class, Answers.RETURNS_DEEP_STUBS);
39 | when(timelineEvent.getStartupStep().getTags()).thenReturn(new TagsContainer(new SimpleTag("beanName", "fooService")));
40 |
41 | AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
42 | tagsResolver.setApplicationContext(ctx);
43 |
44 | Map result = tagsResolver.resolveTags(timelineEvent);
45 |
46 | assertThat(result)
47 | .containsEntry("beanName", "fooService")
48 | .containsEntry("class", "com.demo.FooService");
49 | }
50 |
51 | @Test
52 | void returnsTagWithAnnotations() {
53 | TimelineEvent timelineEvent = Mockito.mock(TimelineEvent.class, Answers.RETURNS_DEEP_STUBS);
54 | when(timelineEvent.getStartupStep().getTags()).thenReturn(new TagsContainer(new SimpleTag("beanName", "fooService")));
55 |
56 | AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
57 | tagsResolver.setApplicationContext(ctx);
58 |
59 | Map result = tagsResolver.resolveTags(timelineEvent);
60 |
61 | assertThat(result)
62 | .containsEntry("annotations", "@Qualifier");
63 | }
64 |
65 | static class AppConfig {
66 |
67 | @Bean
68 | FooService fooService() {
69 | return new FooService();
70 | }
71 | }
72 |
73 | static class TagsContainer implements Tags {
74 | private final List tags;
75 |
76 | public TagsContainer(Tag ... tags) {
77 | this.tags = Arrays.asList(tags);
78 | }
79 |
80 | @Override
81 | public Iterator iterator() {
82 | return tags.iterator();
83 | }
84 |
85 | }
86 |
87 | static class SimpleTag implements Tag {
88 | private final String key;
89 | private final String value;
90 |
91 | public SimpleTag(String key, String value) {
92 | this.key = key;
93 | this.value = value;
94 | }
95 |
96 | @Override
97 | public String getKey() {
98 | return key;
99 | }
100 |
101 | @Override
102 | public String getValue() {
103 | return value;
104 | }
105 |
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/startup-report/src/test/java/com/maciejwalkowiak/spring/boot/startup/TimelineFactoryTest.java:
--------------------------------------------------------------------------------
1 | package com.maciejwalkowiak.spring.boot.startup;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import java.util.List;
6 |
7 | import org.junit.jupiter.api.Test;
8 | import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
9 | import org.springframework.core.metrics.StartupStep;
10 |
11 | public class TimelineFactoryTest {
12 | private final BufferingApplicationStartup startup = new BufferingApplicationStartup(100);
13 | private final TimelineFactory timelineFactory = new TimelineFactory(startup, new TagsResolver());
14 |
15 | @Test
16 | void generatesTimelineWithMultipleTopElements() {
17 | // given
18 | startup.startRecording();
19 | StartupStep step1 = startup.start("event1");
20 | StartupStep step2 = startup.start("event2");
21 | step1.end();
22 | step2.end();
23 | StartupStep step3 = startup.start("event3");
24 | step3.end();
25 |
26 | // when
27 | List events = timelineFactory.getTimeline();
28 |
29 | // then
30 | assertThat(events).hasSize(2);
31 | assertThat(events).anySatisfy(it -> {
32 | assertThat(it.getParentId()).isNull();
33 | assertThat(it.getId()).isZero();
34 | assertThat(it.getName()).isEqualTo("event1");
35 | assertThat(it.getChildren())
36 | .hasSize(1)
37 | .singleElement()
38 | .satisfies(child -> {
39 | assertThat(child.getName()).isEqualTo("event2");
40 | assertThat(child.getId()).isEqualTo(1);
41 | assertThat(child.getParentId()).isEqualTo(it.getId());
42 | });
43 | });
44 | assertThat(events).anySatisfy(it -> {
45 | assertThat(it.getId()).isEqualTo(2);
46 | assertThat(it.getName()).isEqualTo("event3");
47 | assertThat(it.getParentId()).isNull();
48 | });
49 | }
50 |
51 | @Test
52 | void generatesTimelineWithSingleTopElement() {
53 | // given
54 | startup.startRecording();
55 | StartupStep step1 = startup.start("event1");
56 | StartupStep step2 = startup.start("event2");
57 | step2.end();
58 | StartupStep step3 = startup.start("event3");
59 | step3.end();
60 | step1.end();
61 |
62 | // when
63 | List events = timelineFactory.getTimeline();
64 |
65 | // then
66 | assertThat(events).hasSize(1);
67 | Event topElement = events.get(0);
68 | assertThat(topElement.getName()).isEqualTo("event1");
69 | assertThat(topElement.getParentId()).isNull();
70 | assertThat(topElement.getId()).isZero();
71 |
72 | assertThat(topElement.getChildren()).anySatisfy(it -> {
73 | assertThat(it.getName()).isEqualTo("event2");
74 | assertThat(it.getParentId()).isNotNull();
75 | assertThat(it.getId()).isEqualTo(1);
76 | });
77 | assertThat(topElement.getChildren()).anySatisfy(it -> {
78 | assertThat(it.getName()).isEqualTo("event3");
79 | assertThat(it.getParentId()).isNotNull();
80 | assertThat(it.getId()).isEqualTo(2);
81 | });
82 | }
83 | }
84 |
--------------------------------------------------------------------------------