I know this is a common issue and I'm embarrassed to be asking it but I can't work out why I cant load the main class of my multimodule Springboot app.
Full stacktrace:
Exception in thread "main" java.lang.ClassNotFoundException: space.forloop.addon.app.Main
at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:471)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:589)
at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:151)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:398)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:46)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:107)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88)
A have a root-level gradle.build file
plugins {
id 'org.springframework.boot' version '2.4.1'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
repositories {
mavenCentral()
}
bootJar {
mainClass = 'space.forloop.addon.app.Main'
}
def javaProjects = [
'addon-sync-app',
// Removed, not important
]
javaProjects.each {
name ->
project(":$name") {
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'java'
repositories {
// Removed, not important
}
dependencies {
// Removed, not important
}
}
}
In the package module addon-sync-app I have another build.gradle file which just has:
apply plugin: 'org.springframework.boot'
dependencies {
// Removed, not important
}
Looking at the documentation of Configuring the Main Class I was sure adding:
bootJar {
mainClass = 'space.forloop.addon.app.Main'
}
To the root gradle.build file was the correct thing here, but seems not. Any thing else I might have missed?
You are using the Spring Boot plugin in the wrong way. It's such a common mistake that I think they need to document it better or just make it work out-of-the-box. Oh well.
In a Gradle multi-project, you typically define your support libraries and then one or more runnable or deployable applications.
One thing I can't tell from your description is whether you intend the root project to build that final application, or if it should rather be the addon-sync-app project. Given the name, I am assuming the latter, but it can be either one (but generally not both at the same time).
When you apply the Spring Boot plugin to a project (and it doesn't matter if it is the root project or a sub-project), and you rely on defaults, it will take that project and make it into a "fat jar", which requires a special classloader to run. This makes it unsuitable as a normal library. So when you try to depend on it in a normal way, your classes will not be found.
To fix it, you should only apply the Spring Boot plugin to the project that builds the final application jar. If that is addon-sync-app, then remove it from the root and all non-application sub-projects.
If instead, you want the root project to produce the final application, you need to create dependencies to all the required sub-projects and, just as before, remove the Spring Boot plugin from them as well.
One thing you lose when not applying the Spring Boot plugin is the automatic dependency to the BOM which defines default versions of dependencies. If you like to keep using that without creating fat jars of your libraries, there are a few different ways to handle that. I wrote a (little bit too long) answer on that here.
As per gradle documentation, you can try to add 'application' plugin.
https://docs.gradle.org/current/userguide/application_plugin.html
plugins {
id 'application'
}
application {
mainClass = 'space.forloop.addon.app.Main'
}
Related
I am trying to publish a Spring Boot JAR from a subproject, but it fails due to what seems to be eager instantiation within the Gradle Maven Publishing plugin. Here is what I am trying to do:
plugins {
id 'maven-publish'
}
publishing {
publications {
publishSpringBootJar(MavenPublication) {
artifact project(':application').tasks.getByName('bootJar')
}
}
While application is a Spring Boot subproject that has the Spring Boot plugins applies as usual and DOES define a bootJar task when invoked directly. When I invoke the publish task from the parent project I get:
Task with name 'bootJar' not found in project ':application'.
It seems like the Maven publish plugin tries to eagerly insantiate the publication structure without loading tasks within the individual subprojects. What can I do in this case? Could I indirectly reference the Sprint Boot JAR as an artifact without referring directly to the task that generates it and add a publish.dependsOn ':application:bootJar' dependency to the end?
Thank you!
EDIT: The subproject application which is in a subdirectory looks like this:
plugins {
id 'org.springframework.boot' version '2.5.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
...
}
The essence of the problem.
I have several services.
Starter - to start and stop the rest
Service_for_calc - for calculating some operations
Service_sample - service for example
Common_Service-a service for storing models and utils
According to my idea, I will run a starter that will run the rest of the services. Each service will have the same endpoints and models. For example, take the WhoIs functionality.
Each service must tell you who it is.
I don't want to create a model and #Service in every service (module).
I want to create this in Common_service and just import the ready-made logic.
I tried to do this via gradle
to do this in the root settings. gradle I wrote
include 'Service_for_calc'
include 'Common_Service'
include 'Service_sample'
include 'Starter '
and in the service (module) I prescribed it
implementation project(':hub.common')
But I ran into some problem, I can't even describe it clearly, because each time it looked different, but here are the errors that I got when trying to work this way
The module does not see classes from common or does not see the package (despite the fact that the IDE began to suggest them to me)
Some kind of trouble started with the dependency (specifically, Spring dependencies will start working every other time)
Sometimes gradle did not see and threw an error on the implementation project (': hub. common'), with the error that there is no such project ( the name was correct)
After I removed the dependencies, reloaded Gradle and installed it, it worked, but when I tried to open this project on someone else's computer, point 1 was repeated
Maybe I'm doing something wrong, or maybe I shouldn't do it at all.
Is this approach practiced? Was it worth doing it via gradle or should I try it via classpath?
Is it worth doing whole services in a common project?
I will be glad to have a detailed answer!
You have module name conflict, if you have a module named include 'Common_Service' then you should implementation project(':Common_Service')
PS: Here git repo with multi-module, maybe this helps you.
I solve it by root build.gradle
buildscript {
ext {
springBootVersion = '2.4.4'
dependencyManagementVersion = '1.0.11.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
classpath "io.spring.gradle:dependency-management-plugin:${dependencyManagementVersion}"
}
}
allprojects {
group = 'omegabi.back.hub'
version = '0.0.1-SNAPSHOT'
}
subprojects {
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
sourceCompatibility = 11
repositories {
mavenCentral()
}
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-test'
}
}
project(':hub.sample_service') {
dependencies {
compile project(':hub.common_service')
}
}
I solved this problem by deleting settings.gradle in every project except the root one!
We are looking to migrate from Maven to Gradle, and have worked through most of the challenges you would expect for replacing the parent POM concept. There is one sticky point that we haven't figured out yet. We need to specify the version of Spring Boot we are using globally, but I run into invalid build file problems with both of the solutions I've tried:
I tried putting the plugins { id 'org.springframework.boot' version '2.1.17.RELEASE' } declaration in the common build script. Build error, "Only Project and Settings build scripts can contain plugins {} blocks."
I tried calling the common build file to specify the springBootVersion parameter and using that in the plugins declaration. Build Error, "only buildscript {} and other plugins {} script blocks are allowed before plugins {} blocks, no other statements are allowed"
All of this would be easier if I could simply apply plugin: 'org.springframework.boot' but then Gradle can't find the plugin. All but one microservice are on a single version of Spring Boot, and we want to be able to upgrade globally if possible.
Additional Information
I have ~40 microservices plus some libraries used by those services
Separate repository for each of them, so the normal parent/child approach does not work
Maven parent POMs allowed you to publish that POM as it's own resource, and there is no 1:1 equivalent feature in Gradle
Gradle pluginManagement concept also doesn't work for us because it resolves the Spring Boot plugin but the dependency management plugin now can't be found.
My common build script is included here:
repositories {
mavenLocal()
/* Removed our internal repositories */
jcenter()
mavenCentral()
}
apply plugin: 'java'
apply plugin: 'jacoco'
apply plugin: 'maven-publish'
apply plugin: 'io.spring.dependency-management'
group = 'nedl-unified-platform'
/* Required to publish Spring Boot microservices to publish to repository */
configurations {
[apiElements, runtimeElements].each {
it.outgoing.artifacts.removeIf { it.buildDependencies.getDependencies(null).contains(jar) }
it.outgoing.artifact(bootJar)
}
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
withJavadocJar()
withSourcesJar()
}
ext {
set('springBootVersion', '2.1.17.RELEASE')
set('springCloudVersion', "Greenwich.SR6")
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
jacoco {
toolVersion = "0.8.5"
reportsDir = file("$buildDir/reports/jacoco")
}
test {
finalizedBy jacocoTestReport // report is always generated after tests run
}
jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.2
}
}
}
}
jacocoTestReport {
dependsOn test // tests are required to run before generating the report
reports {
xml.enabled true
html.destination file("${reportsDir}/jacocoHtml")
xml.destination file("${reportsDir}/jacocoReport.xml")
}
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
publishing {
publications {
maven(MavenPublication) {
from components.java
}
}
repositories {
/* excluded for privacy and brevity's sake, our internal Maven repo */
}
}
And that is called by our project build script that I want to parameterize:
plugins {
id 'org.springframework.boot' version springBootVersion
}
apply from: "https://mycentral.repo/project-common/develop/build.gradle"
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
implementation 'ch.qos.logback:logback-classic'
implementation 'javax.annotation:javax.annotation-api:1.3.2'
implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359'
implementation 'org.glassfish.jaxb:jaxb-runtime:2.4.0-b180830.0438'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
version = '0.0.2-SNAPSHOT'
I think the gap here is that in maven you have the concept of a parent pom, whereas in Gradle you don't. There is no 1:1 mapping to this like you say, but you can have plugins in Gradle, and apply a plugin.
The closest thing you would have is if you developed your own Gradle plugin, which each of your projects could apply. Your custom plugin would then configure Spring Boot among whatever else is common to all your projects. This plugin would define the version of Spring Boot you want all your other projects to use.
You wouldn't get much benefit to a custom plugin if it's only concern is configuring Spring Boot, it would need to do other things as well. It can be difficult to create a Gradle plugin when you don't have allot of experience in it. You lose all the familiar syntax to the build.gradle and you literally have to write code, (there are some similarities but I have found it difficult), I would avoid it if possible.
I would suggest you start off by applying the spring boot plugin directly to one of your microservices projects, get that working, then do another. After you have done a number of them you will then be able to see what is common between them, and if it is indeed worth investing into developing a global plugin. You really need to be careful though because your global plugin has the potential to be both a blessing and curse. It may take away allot of manual work for maintainers, but if you get it wrong it will cause them grief, and then they will want to go back to maven.
I'm not sure if I understand your globally defined Spring version requirement. Unless you are using SNAPSHOT dependencies/plugins (bad don't do that), (or a black magic settings.gralde outside of your repo), you will have to put some version somewhere. As an alternative you could create your own custom task which runs on the check lifecycle which will check the version of spring (or your plugin) and print a warning if it's not the latest version, and encourage the developer to upgrade.
Extra Info
Parameterizing plugins with properties can be done putting your property in gradle.properties as springBootVersion=2.1.17.RELEASE .
I'm not sure I understood your issue perfectly but you should use the Gradle way for sharing configuration : the root project config.
Instead of including the common build script in every project, create a global project and set the configuration here.
root
|
| --- projectA
| --- projectB
| --- projectC
With the according settings.gradle
include 'projectA'
include 'projectB'
include 'projectC'
In the root build.gradle, set up the version
ext.springBootVersion = '2.1.17.RELEASE'
In subprojects using springBoot, let's say projectB, apply the plugin in the sub build.gradle
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
}
}
apply plugin: 'org.springframework.boot'
This example works for me, though I may not understand all of the constraints.
If we abstract the version of Spring Boot behind a fixed URI (e.g. on an internal CI/CD server), then consider this in each project/repo's build.gradle:
buildscript {
repositories {
jcenter()
}
def SPRING_BOOT_VERSION_URI = 'http://localhost:5151/api-server/spring-boot.txt'
ext.springBootVersion = new URL(SPRING_BOOT_VERSION_URI).getText().trim()
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
}
}
apply plugin: 'org.springframework.boot'
apply from: "../common/build.gradle"
I realize the original question states that the apply plugin doesn't work, but it's not clear to me if that precludes this method.
Finally, note that it is easy to expand this beyond a simple text-file to be a more formal JSON specification (tailored to the teams' needs).
If you add this to the root project, all child projects should be able to just import from the same set of Spring Boot dependencies. The magic ingredient is the allprojects block:
buildscript {
repositories {
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
ext {
springBootVersion = '2.3.4.RELEASE'
}
allprojects {
apply plugin: 'java'
apply plugin: 'io.spring.dependency-management'
dependencyManagement {
imports {
mavenBom("org.springframework.boot:spring-boot-dependencies:${springBootVersion}")
}
}
}
apply plugin: 'org.springframework.boot'
My gradle build file is
plugins {
// Apply the java plugin to add support for Java
id 'java'
// Apply the application plugin to add support for building a CLI application
id 'application'
}
apply plugin: 'java'
jar {
from configurations.runtime
manifest {
attributes(
'Created-By':'Gmack',
'Main-Class':'myapprunner.App',
'Class-Path':'mydaos-1.0.jar'
)
}
}
allprojects{
repositories {
jcenter()
}
}
subprojects {
version = '1.0'
apply plugin: 'java'
}
dependencies {
// This dependency is used by the application.
implementation 'com.google.guava:guava:27.1-jre'
// Use JUnit test framework
testImplementation 'junit:junit:4.12'
// Compile Project for dependency
compile project(':mydaos')
}
application {
// Define the main class for the application
mainClassName = 'myapprunner.App'
}
When I run the app using java -jar myapprunner.jar
I get a ClassNotFoundException
Caused by: java.lang.ClassNotFoundException: com.mydaos.Library
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
... 1 more
I can confirm that the jar has been packed. Not sure why this is not picking things up.
Any help would be appreciated.
Thanks,
Plugin java is being applied twice and com.mydaos.Library is likely being pulled in from compile project(':mydaos') (or 'Class-Path':'mydaos-1.0.jar'). Would assume the project does not build or the class path is wrong.
Dependency classes (projects/external jars) aren't packed inside your jar by default.
You are using the application plugin which bundles your classes, your dependencies and an execution script in a zip so you should use that. The plugin also adds a "run" task to your project to run your main class via gradle for development purposes. See the application plugin docs for more info
If you want to pack your dependencies inside your jar (known as an uber jar) see here. I suggest you stop using the application plugin if you do this
'Class-Path':'mydaos-1.0.jar'
This assumes that mydaos-1.0.jar is in the same folder you are running java -jar ... from which is likely not the case
I am unable to force a version of a dependency using Gradle. My goal is to use version 0.20.0.RELEASE of the Spring HATEOAS library, but despite all my efforts it keeps resolving to 0.19.0.RELEASE.
I have attempted a number of strategies, both in isolation and in combination with one another. These strategies include, but are possibly not limited to, the following (note that in all cases $springHateoasVersionis defined in the gradle.properties file that resides in the directory that is the parent of the directory for the module declaring the Spring HATEOAS dependency):
#1 (in the build.gradle file for the module that declares the dependency)
apply plugin: 'io.spring.dependency-management'
dependencyManagement {
dependencies {
dependency group:'org.springframework.hateoas', name:'spring-hateoas', version:"$springHateoasVersion"
}
}
#2 (in the build.gradle file for the module that declares the dependency)
compile ("org.springframework.hateoas:spring-hateoas:$springHateoasVersion") { force = true }
#3 (in the build.gradle file of the parent directory)
subprojects {
configurations.all {
resolutionStrategy {
force "org.springframework.hateoas:spring-hateoas:$springHateoasVersion"
}
}
}
I have done my best to research this problem:
This question has an accepted answer, but doesn't seem like an exact match for the problem that I'm experiencing: How can I force Gradle to set the same version for two dependencies?
Neither of these questions seem to have accepted answers: 1) Gradle is not honoring resolutionStrategy.force, 2) Forcing a module version has no effect on generated org.eclipse.wst.common.component.
In addition to the fact that my project is broken (because I'm using the wrong version of Spring HATEOAS), I can explicitly see that Gradle is "consciously" selecting the incorrect dependency version despite all my protestations. When I run ./gradlew dependencyInsight --dependency spring-hateoas, I see the following output:
org.springframework.hateoas:spring-hateoas:0.19.0.RELEASE (selected by rule)
org.springframework.hateoas:spring-hateoas:0.20.0.RELEASE -> 0.19.0.RELEASE
\--- project :Commons
\--- compile
Despite the name, the dependencyInsight task provides surprisingly little insight into which rule caused Gradle to select the inappropriate dependency version, let alone how I might go about circumventing said rule.
I found the solution to this problem here. Of course this was the one thing I didn't try because it "didn't seem material". :-/
In order to get things working, I added the following to the build.gradle file of the parent directory (relative to the directory for the module that declared the dependency on Spring HATEOAS).
subprojects {
apply plugin: 'io.spring.dependency-management'
dependencyManagement {
applyMavenExclusions false
}
ext['spring-hateoas.version'] = "$springHateoasVersion"
}
honored by e.g.
allprojects {
repositories {
exclusiveContent {
filter {
includeGroup "com.facebook.react"
}
forRepository {
maven {
url "$rootDir/../node_modules/react-native/android"
}
}
}
}
...
}
ref to https://github.com/facebook/react-native/issues/35204#issuecomment-1304740228