Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to resolve circular dependency in Gradle

Tags:

java

gradle

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.

like image 561
ordonezalex Avatar asked Jun 27 '16 20:06

ordonezalex


People also ask

How do I resolve Gradle dependencies?

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 ( .

What is cyclic dependency and how do you resolve it?

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.

How can circular dependencies be avoided?

Circular dependencies can be introduced when implementing callback functionality. This can be avoided by applying design patterns like the observer pattern.


2 Answers

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.

like image 78
David M. Karr Avatar answered Sep 25 '22 02:09

David M. Karr


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()
    }
  }
}
like image 37
Jhon Doe Avatar answered Sep 25 '22 02:09

Jhon Doe