I am migrating a Java project from Ant to Gradle. I think the best solution is to use Gradle's multi-project support, but I cannot find a way to get rid of a circular dependency.
The original project was setup to have this layout:
- project/
- common/
- product-a/
- product-b/
The relationship between common
, product-a
, and product-b
is tricky. The common
depends on product-a
or product-b
, depending on a configuration file. Likewise, product-a
and product-b
depend on common
, regardless of the configuration property. product-a
and product-b
will never be built at the same time.
I thought a quick solution would be to use something like this in the project/build.gradle
:
project(':product-a') {
dependencies {
compile project(':common')
}
}
project(':product-b') {
dependencies {
compile project(':common')
}
}
Next, I thought about getting a way to get this closer to working for just product-a
. That led me to this:
project(':common') {
dependencies {
compile project(':product-a')
}
}
This will throw an exception for having a circular dependency.
I've considered refactoring product-a
and product-b
by setting up interfaces of the classes expected by common
and product-a
/product-b
or by using polymorphism, but before I move forward with either of those, is there a better way to accomplish this with Gradle? I'm not ready to get rid of this technical debt yet.
Obtaining module metadata Given a required dependency, with a version, Gradle attempts to resolve the dependency by searching for the module the dependency points at. Each repository is inspected in order. Depending on the type of repository, Gradle looks for metadata files describing the module ( .
A cyclic dependency is an indication of a design or modeling problem in your software. Although you can construct your object graph by using property injection, you will ignore the root cause and add another problem: property injection causes Temporal Coupling. Instead, the solution is to look at the design closely.
Circular dependencies can be introduced when implementing callback functionality. This can be avoided by applying design patterns like the observer pattern.
Removing a circular dependency cannot be resolved with build trickery. You're going to have to refactor your modules so there is no longer a circular dependency. From your module names, and with no other information, I would think you would want to extract the part of "common" that depends on "product-*" and put it into a new module.
project(':project-a') {
dependencies {
compile project(':project-b')
}
}
project(':project-b') {
dependencies {
//circular dependency to :project-a
compile project(':project-a')
}
compileJava {
doLast {
// NOTE: project-a needs :project-b classes to be included
// in :project-a jar file hence the copy, mostly done if we need to
// to support different version of the same library
// compile each version on a separate project
copy {
from "$buildDir/classes/java/main"
include '**/*.class'
into project(':project-a').file('build/classes/java/main')
}
}
}
}
product-a --> build.gradle
/**
* Do nothing during configuration stage by
* registering a GradleBuild task
* will be referenced in the task compileJava doLast{}
*/
tasks.register("copyProjectBClasses", GradleBuild) {
//we'll invoke this later
def taskList = new ArrayList<String>()
taskList.add(":product-b:compileJava")
logger.lifecycle "Task to execute $taskList..."
setTasks(taskList)
}
// make sure :project-b classes are compiled first and copied to this project before
// all classes are added to the jar, so we do it after :project-a compiled.
compileJava {
doLast {
synchronized(this) {
// create temp file to avoid circular dependency
def newFile = new File("$buildDir/ongoingcopy.tmp")
if (!newFile.exists()) {
newFile.createNewFile()
GradleBuild buildCopyProjectBClasses = tasks.getByName("copyProjectBClasses")
buildCopyProjectBClasses.build()
}
newFile.delete()
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With