Unable to integration-testing gradle multi-module Spring-Boot-Application - java

Within a Gradle multi-module project with the bootstrapping in its own module I'm unable to use MockMvc, because its need to reference the bootstrapping-module. I'm not sure if I have misconfigured something. The basic structure is:
module: a module containing some REST-Services and needs a testImplementation-Dependency on starter
starter: the bootstrapping-module which gets the spring-boot-plugin applied and depends on module
I have set up a minimal example on github using Spring-Boot 2.3.1.RELEASE and Gradle 6.4 with the following configuration:
./settings.gradle.kts
rootProject.name = "spring-multimodule-integrationtest"
include("starter", "module")
./build.gradle.kts
subprojects {
repositories {
jcenter()
}
dependencies {
apply(plugin = "java-library")
"testImplementation"("junit:junit:4.12")
}
}
./starter/build.gradle.kts
plugins {
id("org.springframework.boot") version "2.3.1.RELEASE"
}
dependencies {
implementation(project(":module"))
}
./module/build.gradle.kts
dependencies {
testImplementation(project(":starter"))
}
The starter-module contains only one a single class "Starter" referencing the module-module:
public class Starter {
public String info() { return "starter"; }
public static void main(String[] args) {
System.out.println(new Starter().info() + " and " + new Module().info());
}
}
The module-module (*sigh I should have chosen a different name for this module) contains only this implemenation-class:
public class Module {
public String info() { return "module"; }
}
Additionally, the module-module has the following test-class doing the integration-test:
public class IntegrationTest
{
#Test public void testSomeLibraryMethod() {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
Starter.main(new String[0]);
assertEquals("starter and module\n", out.toString());
}
}
This code runs fine until the applying of the spring-boot-plugin within "./starter/build.gradle.kts". When the tasks "clean test" issued on the shell I get:
❯ ./gradlew clean test
> Task :module:test FAILED
de.kramhal.multi.IntegrationTest > testSomeLibraryMethod FAILED
java.lang.NoClassDefFoundError at IntegrationTest.java:17
Caused by: java.lang.ClassNotFoundException at IntegrationTest.java:17
1 test completed, 1 failed
This problem does not occur, when tests are executed within the IDE (IntelliJ to be exact).
I already tried unsuccessfully to use the spring-dependency-management as suggested in this answer (as well as in several other answers).
What have I done wrong?

First off, I would recommend restructuring your project so you don't have cyclic dependencies. As it is now, in order to build starter, you need to build module. And in order to test module, you need to build starter. Gradle can do it, but it is usually a smell.
In terms of troubleshooting: when you get a test failure like this, look at the test report as that has the full stack trace. You should see that it complains that it can't find the Starter class (Caused by: java.lang.ClassNotFoundException: de.kramhal.multi.Starter), which is of cause in the starter module.
You mentioned the spring-dependency-management plugin, but that is only relevant for managing Maven dependencies, and not project dependencies like this. So it is not helpful here.
I am not entirely sure if this is Windows specific or not as I remember there were some discussions around performance a while back when having a lot of classes. But I believe the java-library plugin will look for jar files in other projects, and not the folder for compiled classes. This is a problem for you since the spring-boot plugin will by default disable the standard jar task and instead create "fat" a jar file through the bootJar task. Because you need both the fat jar for packaging the application to run stand-alone but also the normal jar for consuming it as a dependency, you need to do some tweaks to the starter project (Kotlin DSL):
tasks {
jar {
enabled = true
}
bootJar {
archiveClassifier.set("boot")
}
}
This will enable the normal jar file, but because the name will conflict with the one produced by the bootJar task, you need to rename one of them. I chose to rename the bootJar one.
I don't know why the test works for you in IntelliJ as that should, by default, delegate everything to Gradle. But maybe you have an old version, or done some manual configuration to let IntelliJ both compile and run your tests.

Related

Gradle set name for jar created with spring-boot's assemble

I want to create an executable jar with gradle (kotlin-dsl) and I want to give it a custom name. For the executable jar I'm using the spring boot plugin and ./gradlew :app1:assemble:
plugins {
id("myproject.java-application-conventions")
id("org.springframework.boot") version "2.2.2.RELEASE"
}
dependencies {
implementation(project(":lib"))
}
application {
mainClass.set("org.myproject.app.Main")
}
init created two more files, buildSrc/build.gradle.kts:
plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
mavenCentral()
}
and buildSrc/src/main/kotlin/myproject.java-application-conventions.gradle.kts:
plugins {
id("lomboker.java-common-conventions")
application
}
With ./gradlew :app1:assemble I can create an executable jar but I don't see how I can set its name.
This question deals with naming jars but I don't know how to apply any answers to my problem.
Adding a jar block to my gradle file does not work: Expression 'jar' cannot be invoked as a function. it is interpreted as sun.tools.jar.resources.jar. So I try tasks.jar instead.
For
tasks.jar {
archiveBaseName.set("myapp")
archiveVersion.set("version")
}
./gradlew :app1:jar while building successful creates no jar (it wouldn't be executable anyway) and ./gradlew :app1:assemble ignores the properties and just creates ./app1/build/libs/app1.jar.
Since I'm not using jar but assemble I guess I should use a tasks.assemble block. But that doesn't recognize archiveBaseName or archiveVersion and I don't know what the API is.
This is the page: https://plugins.gradle.org/plugin/org.springframework.boot but I find no API.
assemble is a lifecycle task which means that it doesn’t create anything. Its role is to trigger other tasks that it depends upon and that do have some output. You can see those tasks by running your build with --console=plain.
The task that creates the Spring Boot fat jar is named bootJar. As you can see from its javadoc, it’s a customization of Gradle’s Jar and can be configured in the same way:
tasks.bootJar {
archiveBaseName.set("myapp")
archiveVersion.set("version")
}

Invocation of MavenCli fails within a Maven plugin

I've created small util to wrap MavenCli, which generates a new Maven project, using the quickstart archetype.
When executing the Util as a unit test, it is working quite well (just generating an empty Maven project).
Now I want to integrate this small wrapper into a Maven plugin. But when I execute the mojo (within a third Maven project), the invocation of MavenCli fails with exception:
[ERROR] Error executing Maven.
[ERROR] java.util.NoSuchElementException
role: org.apache.maven.eventspy.internal.EventSpyDispatcher
roleHint:
[ERROR] Caused by: null
The util looks like:
public void createProject() {
final MavenCli cli = new MavenCli();
System.setProperty("maven.multiModuleProjectDirectory", "/usr/share/maven");
cli.doMain(new String[] { "archetype:generate", "-DgroupId=com.my.company",
"-DartifactId=hello-world", "-DarchetypeArtifactId=maven-archetype-quickstart",
"-DinteractiveMode=false" }, "/tmp", System.out, System.out);
}
relevant dependency of the util:
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-embedder</artifactId>
<version>3.3.9</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>3.3.9</version>
</dependency>
The mojo code looks like:
#Mojo(name = "custommojo", requiresProject = false)
public class CustomMojo extends AbstractMojo {
#Override
public void execute() throws MojoExecutionException, MojoFailureException {
Util.createProject();
}
}
The POM of the mojo just includes dependencies to relevant Maven artifacts (plugin-api, plugin-annotation, etc.) and the util.
The third project I mentioned, is an empty "maven-quickstart" project which have dependency to the mojo-project and a configuration for the mojo to execute in compile phase.
I have no idea why it works in context of unit test, but not in context of a mojo.
Can anybody help?
This is a class loading issue.
MavenCli will try to load classes from the context classloader of the current thread. Inside of a Maven plugin, there is a special, restricted, classloader, which has access to:
its own classes;
the classes used in its dependencies block;
exported classes as part of a possible build extension of the project;
exported classes from the Maven core and core extensions;
and has the bootstrap classloader as parent.
However, the specific class org.apache.maven.eventspy.internal.EventSpyDispatcher is part of Maven core, but it is not part of the exported APIs (the package org.apache.maven.eventspy is not listed as an exportedPackage). So the plugin cannot load that class. This is also why it works in your tests: you're not inside of a plugin, so the classloader is different and has access to that class.
You won't even be able to get away with adding explictly a dependency on maven-core for the plugin: it will be disgarded since it is supposedly already provided.
There are 2 solutions here:
Don't use the Maven Embedder API from within a plugin, but the Invoker API. The difference between the two is that the Invoker API will launch Maven in a clean environment, completely distinct with regard to the current one. Since it starts everything anew, you won't have any classloading issue.
Use the mojo-executor library, that provides an easy way to invoke other Mojos from within a Maven plugin. You could use it here to invoke the archetype:generate Mojo.
This works for me inside a custom maven plugin (using Maven 3.5.0):
ClassRealm classRealm = (ClassRealm) Thread.currentThread().getContextClassLoader();
MavenCli cli = new MavenCli(classRealm.getWorld());
cli.doMain( ... );
The plexus Launcher sets the context class loader to its ClassRealm, which has access to the "global" ClassWorld.
Not sure how stable that solution is, but so far looking good.
Used imports:
import org.codehaus.plexus.classworlds.ClassWorld;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.apache.maven.cli.MavenCli;
#Alexander:
You answer put me in the right direction, I was getting the mentioned error when trying to run to doMain maven commands subsequently (one command would succeed).
However your code gives me a ClassCast exception :
Exception in thread "main" java.lang.ClassCastException: sun.misc.Launcher$AppClassLoader cannot be cast to org.codehaus.plexus.classworlds.realm.ClassRealm
at com.misys.tools.integration.codegen.cli.cmd.InstallApi.mavenInstallMchApi(InstallApi.java:58)
at com.misys.tools.integration.codegen.cli.cmd.InstallApi.run(InstallApi.java:50)
at com.misys.tools.integration.codegen.cli.OpenApiGen.main(OpenApiGen.java:26)
I managed to rewrite the code to:
MavenCli cli = new MavenCli(new ClassWorld("maven",Thread.currentThread().getContextClassLoader()));
And then 2 subsequent doMain invocations of embedded maven succeed!

Shared gradle project in spring boot does not contain yml files

I have projects A and B that both have common project as a compile dependency defined in their build.gradle files like this:
dependencies {
compile project(":common")
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.boot:spring-boot-starter-cache')
compile("net.oauth.core:oauth:20090617")
compile("net.oauth.core:oauth-httpclient4:20090617")
compile('org.apache.httpcomponents:httpclient')
compile("com.atlassian.jira:jira-rest-java-client:2.0.0-m2")
compile("com.google.guava:guava:18.0")
compile('org.flywaydb:flyway-core')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
This common project has application.yml file with all kind of common information such as database connection properties, hibernate setup, etc. So I do not want to duplicate these files all over the other projects such as A and B.
In project A the main spring boot file looks like this:
#SpringBootApplication(scanBasePackageClasses = {CommonApp.class,
A.class})
public class A {
public static void main(String[] args) {
SpringApplication.run(A.class, args);
}
}
where CommonApp is a main class in the commmon project. This common main file is the following:
#SpringBootApplication
public class CommonApp {
public static void main(String[] args) {
SpringApplication.run(CommonApp.class, args);
}
}
Projects A and B compile just fine, but all yml files that are in the classpath of the common project are not visible from A and B, so I have not choice but to duplicate them manually in A and B
What is the better approach? Can spring boot common projects share resources with other projects?
Notice that ideally solution should not be gradle dependant as I would like to run unit and integration tests from Intellij IDEA which does not use gradle for running tests.
My app structure is
app
|-A
|-build.gradle
|-web
|-B
|-build.gradle
|-common
|-src/main/resources
|-application.yml
|-database.yml
|-web.yml
|-settings.gradle
|-build.gradle
A, B and common are all spring boot apps (common is a boot app too, but it's only used as a dependency for A, B).
If you want a non-gradle solution, you could always add a DAO-esque file to access the required values in the common project then jar your common project and add it as a dependency to projects A and B.
Edit: I apologize, I did not explain very well. When I say "DAO-esque file", what I mean to say is a file of getter (and setters if needed) that access the properties file. For example, have a getter file that is a singleton. On creation, create a static reference to the property file. When Project A (or B) needs a property, it can call to this file to get the property.
As for a gradle solution, it looks like your above is fairly close with your
compile project(":common")
code. Do you have a settings.gradle file that has the line
includeFlat "common"
and a reference to the common project in the same directory as Project A and B? For example
Project A
(...Project A's files...)
Project B
(...Project B's files...)
Common
(...Common's files...)

In Gradle, associating task with a dependency configuration

How can a task be associated to a specific dependency configuration?
If I look the 23.5. Dependency management (gradle java plugin official doc) section part, it states that, for example, compileTestJava task use testCompile configuration.
I just wanted to know how I could achieve that.
gradle is creating these configurations automatically;
if you define a sourceSet, a bunch of things gets created (by convention):
sourceSets {
thing
}
will define configurations: thingCompile, thingRuntime
tasks: compileThingJava, processThingResources, thingClasses
you might want to look at: gradle tasks --all and gradle dependencies
if you want to add dependencies to these configurations
the most preferable to use the generated ones
you may of course create your own configuration and extend from that: configurations { thingCompile.extendsFrom(myConfig) }

Gradle tomcat plugin and properties files

I would like to use the gradle tomcat plugin in order to do integration tests with gradle. The current project relies on some .properties files underneath the running tomcat's catalina.base directory (cannot be changed because another dependent project relies on them as well).
Does anybody know how to deploy those files to the embedded tomcat instance?
I figured out it's just a simple copy task issue. Here's my solution:
task copyDMConfigFiles << {
def srcDir = new File('src/test/resources/conf')
if(!srcDir.isDirectory())
println "Outlet configuration files missing!!!"
def buildDir = new File('build/tmp/tomcatRunWar/conf')
if(!buildDir.isDirectory()) {
println "Outlet target directory missing. Creating one"
buildDir.mkdirs()
}
copy {
from(srcDir)
into(buildDir)
include '**/*.properties'
include '**/*.xml'
}
copy {
from('src/main/webapp/WEB-INF')
into('build/tmp/tomcatRunWar/work/Tomcat/localhost/digitalmedia/WEB-INF')
include 'web.xml'
include 'dispatcherservlet.xml'
}
}

Categories