Solve BOM existential crisis - Fixes #15258
To bom or not to bom?
This fix allows me to go to sleep tonight...
- Fixes version resolution
- Sorts versions
- Disables bom module file that breaks dependency management and actually forces wrong versions on transitive dependencies
- Removes 131 redundant versions
https://github.com/apache/grails-core/issues/15258
Gradle modules are not needed for the bom and prohibit the dependency management plugin
Working M3 Bom Artifacts Broken M4 Bom Artifacts
grails-bom-7.0.0-M3.pom 27207 <- everything working, no module file
grails-bom-7.0.0-M4.pom 28845
grails-bom-7.0.0-M4.module 52007 <- forces versions on everything, version overrides broken
grails-bom-7.0.0-RC2.pom 41614 <- adds 131 versions
grails-bom-7.0.0-RC2.module 54584
grails-bom-7.0.3.pom 26621 <- removes all versions from everything
grails-bom-7.0.3.module 52007
grails-bom-7.1.0-SNAPSHOT.pom 29781 <- no module, allows version overrides
After this fix you can override any version just by modifying gradle.properties
This was not possible in 7.0.0-M4 -> 7.0.3
spring-boot.version=3.5.8
groovy.version=4.0.29
Spring Boot Does Not Use a .module file with their bom:
3.5.8
4.0.0-RC2
As an incremental fix, we should first merge:
https://github.com/apache/grails-core/pull/15260 which requires https://github.com/apache/grails-gradle-publish/pull/16
Not having a deferred lookup is the main reason properties were broken in the bom.
As an incremental fix, we should first merge:
#15260 which requires apache/grails-gradle-publish#16
Not having a deferred lookup is the main reason properties were broken in the bom.
This still
- Sorts versions
- Disables bom module file that breaks dependency management and actually forces wrong versions on transitive dependencies
- Removes 131 redundant versions
After publishing the other changes, the bom with properties can be seen here: https://repository.apache.org/service/local/repo_groups/snapshots-group/content/org/apache/grails/grails-bom/7.0.4-SNAPSHOT/grails-bom-7.0.4-20251126.132828-4.pom
Concerning this comment:
Number 2 is false. The spring dependency management plugin works just fine with the module metadata published. You can see this in the example project here: https://github.com/jdaugherty/grails-bom-demo-spring-dependency-management (this project downgrades spring boot with a property setting only). Again, you must not use the platform if you want the property behavior. We intentionally shipped the platform() because there isn't an alternative in gradle build script & to be consistent we defined it in both locations. We intend to remove the spring dependency management plugin in Grails 8.
For sorting versions, I'm indifferent and I think it's ok to accept.
For the 131 redundant versions, by redundant I assume you mean that a grails.version property isn't defined for the grails project? Technically if you know what you're doing, you could selectively upgrade one of those libraries with the way this is defined. This allows for the most flexibility. I think this is something that should be discussed since we launched 7.0.0 with this design. If you want to change the overall grails version, you would just select a different bom version. The current state allows for the most flexibility.
@codeconsole I changed the example project to make it clear that i removed platform() - the initial commit now contains the default app generated in grails forge and the second contains the way you would setup your project to use the dependency management plugin.
Number 2 is false. The spring dependency management plugin works just fine with the module metadata published. You can see this in the example project here: https://github.com/jdaugherty/grails-bom-demo-spring-dependency-management (this project downgrades spring boot with a property setting only). Again, you must not use the platform if you want the property behavior. We intentionally shipped the platform() because there isn't an alternative in gradle build script & to be consistent we defined it in both locations. We intend to remove the spring dependency management plugin in Grails 8.
@jdaugherty you are right, I was able to get RC2 working. I think the big error there was spring.boot.version was renamed to spring.boot.dependencies.version, but it also works with platform.
implementation platform("org.apache.grails:grails-bom:$grailsVersion") has no impact on the version being set
For the 131 redundant versions, by redundant I assume you mean that a grails.version property isn't defined for the grails project? Technically if you know what you're doing, you could selectively upgrade one of those libraries with the way this is defined. This allows for the most flexibility. I think this is something that should be discussed since we launched 7.0.0 with this design. If you want to change the overall grails version, you would just select a different bom version. The current state allows for the most flexibility.
@jdaugherty The emphasis here is I don't see a working scenario where you would want to set any of those versions via a property because their transitive dependencies would resolve a different version. Since they are all based off a Grails version, setting to a different version would cause unexpected transitive resolution.
Number 2 is false. The spring dependency management plugin works just fine with the module metadata published. You can see this in the example project here: https://github.com/jdaugherty/grails-bom-demo-spring-dependency-management (this project downgrades spring boot with a property setting only). Again, you must not use the platform if you want the property behavior. We intentionally shipped the platform() because there isn't an alternative in gradle build script & to be consistent we defined it in both locations. We intend to remove the spring dependency management plugin in Grails 8.
@jdaugherty you are right, I was able to get RC2 working. I think the big error there was
spring.boot.versionwas renamed tospring.boot.dependencies.version, but it also works with platform.
implementation platform("org.apache.grails:grails-bom:$grailsVersion")has no impact on the version being set
The original goal of the bom changes was so we could document it & define dependencies in a central place - especially because we generate two boms (grails-gradle-bom & grails-bom). I used a prefix naming strategy originally because it was claimed that dependabot could handle versions & coordinates in the same gradle file - so I wanted to keep it simple for dependabot. What that did not mention is it only handles String versions & can't handle the map syntax. So we can rework this ...
For Gradle 9, we have to rewrite all of it anyhow - gradle doesn't allow across project resolution as of Gradle 9 (see the versions plugin & associated ticket where this was discovered without a release note in Gradle 9). We need to extract the bom logic into it's own plugin using maven specific libraries to parse poms instead of gradle - similar to what spring did and then generate documentation & the bom from that plugin. We can hack in the old name if you want, but the current property is based on the coordinate name.
As for it working with both, that's great news. We should revert the metadata disable then.
For the 131 redundant versions, by redundant I assume you mean that a grails.version property isn't defined for the grails project? Technically if you know what you're doing, you could selectively upgrade one of those libraries with the way this is defined. This allows for the most flexibility. I think this is something that should be discussed since we launched 7.0.0 with this design. If you want to change the overall grails version, you would just select a different bom version. The current state allows for the most flexibility.
@jdaugherty The emphasis here is I don't see a working scenario where you would want to set any of those versions via a property because their transitive dependencies would resolve a different version. Since they are all based off a Grails version, setting to a different version would cause unexpected transitive resolution.
Isn't that only true if the transitive dependency is in the bom? What if someone wanted to pull in fields because a new default template was added - then that dependency doesn't matter. I could see this be true for a lot of grails projects.
We discussed this PR in the weekly meeting. Given that the multiple properties allow for the option to customize select libraries, we want to keep that feature.
I believe the only other feature this PR contributed was sorting the properties. If you want to update the PR for that we can merge this. Otherwise, we'll plan on closing this one.
We discussed this PR in the weekly meeting. Given that the multiple properties allow for the option to customize select libraries, we want to keep that feature.
I believe the only other feature this PR contributed was sorting the properties. If you want to update the PR for that we can merge this. Otherwise, we'll plan on closing this one.
@jdaugherty can you give an actual use case where you would use these properties? There is no grouping on any of them which makes no difference if you import them directly.
<grails.async.gpars.version>7.1.0-SNAPSHOT</grails.async.gpars.version>
<grails.events.gpars.version>7.1.0-SNAPSHOT</grails.events.gpars.version>
why would you set
grails.async.gpars.version=7.1.0-SNAPSHOT
instead of just putting it in the buld.gradle directly? what is the point?
implementation 'org.apache.grails.asyncs:grails-async-gpars:7.1.0-SNAPSHOT'
I could, possibly, understand grails.async.version, but is there really a use case for introducing 130 individual properties with the same exact version? What is the point of that?
The decision to keep 130 properties sounds like it is going to introduce a lot of unknown behavior by the end user.
The reason we kept the dependency management plugin is it uses properties to be able to quickly set versions. There's a version for each project because each project is a separate jar.
The only reason they're the same version is because of the mono repo, but technically people can choose different versions. There also isn't "unknown" behavior by leaving properties in a pom. We're opting to keep the properties to keep the flexibility of prior grails versions.
@codeconsole I haven't seen an example of where it's bad to keep the flexibility of the pom properties. I also haven't seen any updates to this PR. do you wish to abandon it?
@codeconsole I haven't seen an example of where it's bad to keep the flexibility of the pom properties. I also haven't seen any updates to this PR. do you wish to abandon it?
I am finding it quite hard to understand why you would ever set a version for an individual dependency instead of a group?
What value does that serve? Isn't it overly redundant?
With my previous bom code, versions were set on groups, which did have value.
As I asked previously:
what is the advantage of
grails.async.gpars.version=7.1.0-SNAPSHOT
over
implementation 'org.apache.grails.asyncs:grails-async-gpars:7.1.0-SNAPSHOT'
why would you ever do the former??
If the property is the same for all Grails modules, couldn't you just use the org.apache.grails:grails-bom with that version instead?
@codeconsole To answer your question: the advantage of the property approach is you can define properties in more ways independently of the project. This is the only reason we kept the dependency management plugin. As long as we keep that plugin, we should keep the property support since it's central to including that plugin.
Also, as for why someone would want to upgrade, I have given previous examples of that - fields or more isolated downstream projects could very well be updated instead of something like grails-core.
So the property support + arbitrary upgrades is a valid use case.
@jdaugherty I am still not understanding your explanation for these 130 properties that all have the same version 7.1.0-SNAPSHOT Can you please provide an example when you would use any of them and the value of doing that over just specifying the dependency in build.gradle?
Properties in boms are typically for dependency groups, not individual child dependencies of a project.
With my previous bom code, versions were set on groups, which did have value.
As I asked previously:
what is the advantage of
grails.async.gpars.version=7.1.0-SNAPSHOTover
implementation 'org.apache.grails.asyncs:grails-async-gpars:7.1.0-SNAPSHOT'why would you ever do the former??
If the property is the same for all Grails modules, couldn't you just use the
org.apache.grails:grails-bomwith that version instead?
you could, but them you would get that version's bom dependencies. I would argue more for removing the 100+ module properties altogether.
I think the bom now is overwhelmed with so many redundant properties.
I'd recommend any of the following:
- Go back to the previous format that actually grouped them and providing something useful. However, the value is probably quite limited since they are all released as the same version.
org.grails.plugins:gsp::
org.grails:grails-datastore-gorm-hibernate5::
org.grails:grails-datastore-gorm-mongodb::
org.grails:grails-datastore-async,grails-datastore-core,grails-datastore-gorm,grails-datastore-gorm-a
sync,grails-datastore-gorm-support,grails-datastore-gorm-rx,grails-datastore-gorm-test,grails-datastore-gorm-
validation,grails-datastore-web,grails-datastore-gorm-tck,grails-gorm-testing-support:::grails-datastore
- Use the
grails.version - Remove the properties from the bom and just hardcode the version.
It's not about which of these is better. Since Grails 7 applications are still generated with the Spring Dependency Management Grails Plugin, we support both pathways to override the grails artifact versions.
In addition to just using the new grails-bom version, which 99.999% of user will do.
Overriding just one or a few of these artifacts will rarely be done, but we must maintain support for it via
-
Spring Dependency Management Plugin Properties
grails.async.gpars.version=7.1.0-SNAPSHOT -
Adjusting the dependency directly
implementation 'org.apache.grails.asyncs:grails-async-gpars:7.1.0-SNAPSHOT'
Users can pick which path they want to follow and having both is consistent with prior Grails versions.
Removing the grails.*.*.version properties from the bom will remove the ability to adjust the versions via properties which we can't do in the middle of 7.x.x releases: https://docs.spring.io/spring-boot/3.5/gradle-plugin/managing-dependencies.html#managing-dependencies.dependency-management-plugin.customizing
For Grails 8, https://github.com/apache/grails-core/issues/14142 will be addressed.
It's not about which of these is better. Since Grails 7 applications are still generated with the Spring Dependency Management Grails Plugin, we support both pathways to override the grails artifact versions.
This isn't a question of better. It's about proper bom design and using bom versions how they are intended. Versions are more there to specify versions for dependency groups, not individual submodules. There is no history of these 100+ versions existing. Why introduce them now? It's variable overkill. This looks more like a bug than a feature.
The correct approach would either be: 1. remove them completely or 2. create the appropriate groups (which is how historically it was done).
Please explain what the expected behavior is in it's current design for:
grails.async.gpars.version=7.1.0-SNAPSHOT
will this also update grails async or just grails.async.gpars? if so, is the correct property name grails.async?
Is this introduction of a 130 individual versions documented anywhere with usage instructions?
@codeconsole You're not presenting a technical argument here. We answered your questions:
- There are more versions because we went to a mono repo. We decided to do this based on continuing to recommend the dependency management plugin & it's support for property based configuration.
- As for the naming, this decision was to make it easy to configure based on the grails project name. We went with this in 7.0 so we can't change it now. The time to raise the name problem would have been prior to the 7.0 release.
- As for a specific example, you seem to have not accepted the example I gave concerning fields. Could we have grouped these, yes. We decided against it to be more flexible.
- As for adding a dependency via gradle instead, that isn't what's under question. These versions exist only because of the property support in dependency management. You don't even have to change code with the property support. An environment variable of
ORG_GRADLE_grails.fields.version=7.0.3would override the version. Removing the properties, removes this support.
I'm a -1 on this PR / code change until a technical reason can be given on why we should limit end apps flexibility & choice.
@jdaugherty The 130 versions doesn't have anything to do with the mono-repo. It has to do with, all of the sudden, introducing variables for versions specific to the project. There should never be a 1-1 correlation between dependencies and version variables. Can you give an example of anyone else that does this??
project submodules versions should be hard coded project dependencies should be grouped
This is how things worked previously.
Have you looked at the Spring Boot Bom? What I am suggesting models exactly what Spring Boot does and was how everything worked before.
For example, if Spring were to follow your suggestion, they would need to introduce 100's of versions as well:
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-resttestclient</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-rsocket</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-rsocket-test</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-security</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-security-oauth2-authorization-server</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-security-oauth2-client</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-security-oauth2-resource-server</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-security-saml2</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-security-test</artifactId>
<version>4.0.1</version>
</dependency>
...
If Spring did what you were suggesting, they would have to add versions to their bom for:
spring-boot,spring-boot-activemq,spring-boot-actuator,spring-boot-actuator-autoconfigure,spring-boot-amqp,spring-boot-artemis,spring-boot-autoconfigure,spring-boot-autoconfigure-classic,spring-boot-autoconfigure-classic-modules,spring-boot-autoconfigure-processor,spring-boot-batch,spring-boot-batch-jdbc,spring-boot-buildpack-platform,spring-boot-cache,spring-boot-cache-test,spring-boot-cassandra,spring-boot-cloudfoundry,spring-boot-configuration-metadata,spring-boot-configuration-processor,spring-boot-couchbase,spring-boot-data-cassandra,spring-boot-data-cassandra-test,spring-boot-data-commons,spring-boot-data-couchbase,spring-boot-data-couchbase-test,spring-boot-data-elasticsearch,spring-boot-data-elasticsearch-test,spring-boot-data-jdbc,spring-boot-data-jdbc-test,spring-boot-data-jpa,spring-boot-data-jpa-test,spring-boot-data-ldap,spring-boot-data-ldap-test,spring-boot-data-mongodb,spring-boot-data-mongodb-test,spring-boot-data-neo4j,spring-boot-data-neo4j-test,spring-boot-data-r2dbc,spring-boot-data-r2dbc-test,spring-boot-data-redis,spring-boot-data-redis-test,spring-boot-data-rest,spring-boot-devtools,spring-boot-docker-compose,spring-boot-elasticsearch,spring-boot-flyway,spring-boot-freemarker,spring-boot-graphql,spring-boot-graphql-test,spring-boot-groovy-templates,spring-boot-gson,spring-boot-h2console,spring-boot-hateoas,spring-boot-hazelcast,spring-boot-health,spring-boot-hibernate,spring-boot-http-client,spring-boot-http-codec,spring-boot-http-converter,spring-boot-integration,spring-boot-jackson,spring-boot-jackson2,spring-boot-jarmode-tools,spring-boot-jdbc,spring-boot-jdbc-test,spring-boot-jersey,spring-boot-jetty,spring-boot-jms,spring-boot-jooq,spring-boot-jooq-test,spring-boot-jpa,spring-boot-jpa-test,spring-boot-jsonb,spring-boot-kafka,spring-boot-kotlinx-serialization-json,spring-boot-ldap,spring-boot-liquibase,spring-boot-loader,spring-boot-mail,spring-boot-micrometer-metrics,spring-boot-micrometer-metrics-test,spring-boot-micrometer-observation,spring-boot-micrometer-tracing,spring-boot-micrometer-tracing-brave,spring-boot-micrometer-tracing-opentelemetry,spring-boot-micrometer-tracing-test,spring-boot-mongodb,spring-boot-mustache,spring-boot-neo4j,spring-boot-netty,spring-boot-opentelemetry,spring-boot-persistence,spring-boot-properties-migrator,spring-boot-pulsar,spring-boot-quartz,spring-boot-r2dbc,spring-boot-reactor,spring-boot-reactor-netty,spring-boot-restclient,spring-boot-restclient-test,spring-boot-restdocs,spring-boot-resttestclient,spring-boot-rsocket,spring-boot-rsocket-test,spring-boot-security,spring-boot-security-oauth2-authorization-server,spring-boot-security-oauth2-client,spring-boot-security-oauth2-resource-server,spring-boot-security-saml2,spring-boot-security-test,spring-boot-sendgrid,spring-boot-servlet,spring-boot-session,spring-boot-session-data-redis,spring-boot-session-jdbc,spring-boot-sql,spring-boot-starter,spring-boot-starter-activemq,spring-boot-starter-activemq-test,spring-boot-starter-actuator,spring-boot-starter-actuator-test,spring-boot-starter-amqp,spring-boot-starter-amqp-test,spring-boot-starter-artemis,spring-boot-starter-artemis-test,spring-boot-starter-aspectj,spring-boot-starter-aspectj-test,spring-boot-starter-batch,spring-boot-starter-batch-jdbc,spring-boot-starter-batch-jdbc-test,spring-boot-starter-batch-test,spring-boot-starter-cache,spring-boot-starter-cache-test,spring-boot-starter-cassandra,spring-boot-starter-cassandra-test,spring-boot-starter-classic,spring-boot-starter-cloudfoundry,spring-boot-starter-cloudfoundry-test,spring-boot-starter-couchbase,spring-boot-starter-couchbase-test,spring-boot-starter-data-cassandra,spring-boot-starter-data-cassandra-test,spring-boot-starter-data-cassandra-reactive,spring-boot-starter-data-cassandra-reactive-test,spring-boot-starter-data-couchbase,spring-boot-starter-data-couchbase-test,spring-boot-starter-data-couchbase-reactive,spring-boot-starter-data-couchbase-reactive-test,spring-boot-starter-data-elasticsearch,spring-boot-starter-data-elasticsearch-test,spring-boot-starter-data-jdbc,spring-boot-starter-data-jdbc-test,spring-boot-starter-data-jpa,spring-boot-starter-data-jpa-test,spring-boot-starter-data-ldap,spring-boot-starter-data-ldap-test,spring-boot-starter-data-mongodb,spring-boot-starter-data-mongodb-test,spring-boot-starter-data-mongodb-reactive,spring-boot-starter-data-mongodb-reactive-test,spring-boot-starter-data-neo4j,spring-boot-starter-data-neo4j-test,spring-boot-starter-data-r2dbc,spring-boot-starter-data-r2dbc-test,spring-boot-starter-data-redis,spring-boot-starter-data-redis-test,spring-boot-starter-data-redis-reactive,spring-boot-starter-data-redis-reactive-test,spring-boot-starter-data-rest,spring-boot-starter-data-rest-test,spring-boot-starter-elasticsearch,spring-boot-starter-elasticsearch-test,spring-boot-starter-flyway,spring-boot-starter-flyway-test,spring-boot-starter-freemarker,spring-boot-starter-freemarker-test,spring-boot-starter-graphql,spring-boot-starter-graphql-test,spring-boot-starter-groovy-templates,spring-boot-starter-groovy-templates-test,spring-boot-starter-gson,spring-boot-starter-gson-test,spring-boot-starter-hateoas,spring-boot-starter-hateoas-test,spring-boot-starter-hazelcast,spring-boot-starter-hazelcast-test,spring-boot-starter-integration,spring-boot-starter-integration-test,spring-boot-starter-jackson,spring-boot-starter-jackson-test,spring-boot-starter-jdbc,spring-boot-starter-jdbc-test,spring-boot-starter-jersey,spring-boot-starter-jersey-test,spring-boot-starter-jetty,spring-boot-starter-jetty-runtime,spring-boot-starter-jms,spring-boot-starter-jms-test,spring-boot-starter-jooq,spring-boot-starter-jooq-test,spring-boot-starter-json,spring-boot-starter-jsonb,spring-boot-starter-jsonb-test,spring-boot-starter-kafka,spring-boot-starter-kafka-test,spring-boot-starter-kotlinx-serialization-json,spring-boot-starter-kotlinx-serialization-json-test,spring-boot-starter-ldap,spring-boot-starter-ldap-test,spring-boot-starter-liquibase,spring-boot-starter-liquibase-test,spring-boot-starter-log4j2,spring-boot-starter-logback,spring-boot-starter-logging,spring-boot-starter-mail,spring-boot-starter-mail-test,spring-boot-starter-micrometer-metrics,spring-boot-starter-micrometer-metrics-test,spring-boot-starter-mongodb,spring-boot-starter-mongodb-test,spring-boot-starter-mustache,spring-boot-starter-mustache-test,spring-boot-starter-neo4j,spring-boot-starter-neo4j-test,spring-boot-starter-oauth2-authorization-server,spring-boot-starter-oauth2-client,spring-boot-starter-oauth2-resource-server,spring-boot-starter-opentelemetry,spring-boot-starter-opentelemetry-test,spring-boot-starter-pulsar,spring-boot-starter-pulsar-test,spring-boot-starter-quartz,spring-boot-starter-quartz-test,spring-boot-starter-r2dbc,spring-boot-starter-r2dbc-test,spring-boot-starter-reactor-netty,spring-boot-starter-restclient,spring-boot-starter-restclient-test,spring-boot-starter-rsocket,spring-boot-starter-rsocket-test,spring-boot-starter-security,spring-boot-starter-security-test,spring-boot-starter-security-oauth2-authorization-server,spring-boot-starter-security-oauth2-authorization-server-test,spring-boot-starter-security-oauth2-client,spring-boot-starter-security-oauth2-client-test,spring-boot-starter-security-oauth2-resource-server,spring-boot-starter-security-oauth2-resource-server-test,spring-boot-starter-security-saml2,spring-boot-starter-security-saml2-test,spring-boot-starter-sendgrid,spring-boot-starter-sendgrid-test,spring-boot-starter-session-data-redis,spring-boot-starter-session-data-redis-test,spring-boot-starter-session-jdbc,spring-boot-starter-session-jdbc-test,spring-boot-starter-test,spring-boot-starter-test-classic,spring-boot-starter-thymeleaf,spring-boot-starter-thymeleaf-test,spring-boot-starter-tomcat,spring-boot-starter-tomcat-runtime,spring-boot-starter-validation,spring-boot-starter-validation-test,spring-boot-starter-web,spring-boot-starter-web-services,spring-boot-starter-webclient,spring-boot-starter-webclient-test,spring-boot-starter-webflux,spring-boot-starter-webflux-test,spring-boot-starter-webmvc,spring-boot-starter-webmvc-test,spring-boot-starter-webservices,spring-boot-starter-webservices-test,spring-boot-starter-websocket,spring-boot-starter-websocket-test,spring-boot-starter-zipkin,spring-boot-test,spring-boot-test-autoconfigure,spring-boot-test-classic-modules,spring-boot-testcontainers,spring-boot-thymeleaf,spring-boot-tomcat,spring-boot-transaction,spring-boot-validation,spring-boot-web-server,spring-boot-webclient,spring-boot-webclient-test,spring-boot-webflux,spring-boot-webflux-test,spring-boot-webmvc,spring-boot-webmvc-test,spring-boot-webservices,spring-boot-webservices-test,spring-boot-websocket,spring-boot-webtestclient,spring-boot-zipkin,spring-boot-starter-zipkin-test
but they didn't. Why is that? Are you saying what Spring Boot is doing "limit end apps flexibility & choice"?