StackHawk



Sharing Dependencies and Gradle Plugins between Kotlin/SpringBoot Services

Topher Lamey   |   May 18, 2020

LinkedIn
X (Twitter)
Facebook
Reddit
Subscribe To StackHawk Posts

The Backstory

When we were bootstrapping things at StackHawk, we made the decisions to:

  • Not use a mono-repo

  • Avoid dependencies across repos based on filesystem layout assumptions

That did require us to handle shared dependencies right up front, but the payoff is that things are scaling nicely as services and team members are added. For our SpringBoot/Kotlin/Gradle projects, this meant each one had their ownplugins
anddependencies
blocks that look like this:
plugins {
   id("org.springframework.boot") version "2.2.1.RELEASE"
   id("io.spring.dependency-management") version "1.0.8.RELEASE
   [...]
}
 
dependencies {
   implementation("org.springframework.boot:spring-boot-starter-data-jpa")
   implementation("org.springframework.boot:spring-boot-starter-mustache")
  [...]
}

Blog Banner - Find and Fix Security Vulnerabilities Banner

The Drifting Problem

Fast-forward a bit and now that we have more than a few services in their own repos, each with their ownplugins
anddependencies
blocks, we have a drift problem. We recently wanted all projects to be on a specific version of the SpringBoot plugin, and we realized that updating every project on its own is tedious and error-prone. Additionally, we noticed that despite our best efforts, the Gradle configs across our relatively young projects had drifted – versions were out of sync, some projects were missing plugins, etc.

This kind of drift can cause subtle issues around expected behavior between projects; if someone in Project A builds functionality around a library function, and then they move to Project B that’s using a different version with different behavior. Or if someone is using a development tool in Project X that automatically safeguards against a condition, and then they move to Project Y that does not have that tool wired in and they erroneously assume they don’t need to deal with the condition.

Back On Track

Gradle generally wants multi-project builds to be in the same repo, using filesystem-relative things likeinclude
,project
, orincludeFlat
to pull common configs together. But since we don’t have a mono-repo and we do not want to assume multi-project filesystem layouts, we didn’t want to use those Gradle features.

Naturally, Gradle does support sharing plugins and dependencies via published artifacts using the Maven coordinate convention, but it requires a little bit of work. Turns out, a custom plugin is needed for plugins, and a custom platform is needed for dependencies. Both of these were easy to build and incorporate into our codebase.

Custom Plugin

Plugins can be shared across projects by writing a custom plugin in its own project.

  • Set up a custom plugin project using themaven-plugin and java-gradle-plugin
    plugins in thebuild.gradle.kts file
  • Write a custom plugin that extendsPlugin<Project>
  • In theapply method of the custom plugin, use theproject.plugins object toapply plugins you want shared via their respective plugin id
  • Specify the dependent plugin libraries via their classpath Maven coordinates that are referenced by their plugin id in the custom plugin’sbuild.gradle.kts
    file.
    1. Note that the plugin ID and the classpath coordinates are different things and that https://plugins.gradle.org/ will give you both pieces of info.

  • Use themaven-publishing plugin to publish the custom plugin to a Maven repo
  • In the dependent project, pull in the custom plugin by its plugin id in theplugins block of thebuild.gradle.kts
  • In order for the dependent project to find this new plugin via it’s id in the Maven repo, you’ll need to add your repo to the plugin configuration. We did this in the dependent project’ssettings.gradle.kts, in thepluginManagement block.

Example custom pluginbuild.gradle.kts
plugins {
    `maven-publish`
    `java-gradle-plugin`
    kotlin("jvm") version "1.3.70"
}
 
group = "com.stackhawk.plugins.example"
version = "0.0.1"
java.sourceCompatibility = JavaVersion.VERSION_1_8
 
repositories {
    // Need to include plugin repo
    gradlePluginPortal()
    mavenCentral()
    mavenLocal()
    jcenter()
}
 
dependencies {
    // Plugin classpath artifacts with specific versions
    implementation("org.springframework.boot:spring-boot-gradle-plugin:2.2.5.RELEASE")
    implementation("io.spring.gradle:dependency-management-plugin:1.0.9.RELEASE")
 
    val kotlinPluginVersion = "1.3.70"
    implementation(kotlin("gradle-plugin:${kotlinPluginVersion}"))
    implementation(kotlin("allopen:${kotlinPluginVersion}"))
    implementation(kotlin("noarg:${kotlinPluginVersion}"))
}
 
publishing {
    repositories {
        maven {
            url = uri("http://private-repo/")
        }
    }
}
 
gradlePlugin {
    plugins {
        create("example") {
            // This ID is used by the dependent projects in the plugins block
            id = "com.stackhawk.example"
            implementationClass = "com.stackhawk.gradle.ExamplePlugin"
        }
    }
}

Example custom plugin class
package com.stackhawk.gradle
 
import org.gradle.api.Plugin
import org.gradle.api.Project
 
class ExamplePlugin : Plugin<Project> {
 
    override fun apply(project: Project) {
        // Shared Plugin IDs
        // Also add the Plugin's classpath artifact in build.gradle.kts
        project.plugins.apply("idea")
        project.plugins.apply("jacoco")
        project.plugins.apply("org.springframework.boot")
        project.plugins.apply("org.springframework.boot")
        project.plugins.apply("io.spring.dependency-management")
 
        project.plugins.apply("org.jetbrains.kotlin.jvm")
        project.plugins.apply("org.jetbrains.kotlin.plugin.spring")
        project.plugins.apply("org.jetbrains.kotlin.plugin.jpa")
        project.plugins.apply("org.jetbrains.kotlin.plugin.noarg")
    }
}

Example dependent projectbuild.gradle.kts plugins block

plugins {
 id("com.stackhawk.gradle") version "0.0.4"
}

Example dependent projectsettings.gradle.kts

rootProject.name = "yarak"
 
pluginManagement {
   // Needed to find our new custom plugin
   repositories {
       gradlePluginPortal()
       mavenLocal()
       maven {
           url = uri("http://private-repo/")
       }
   }
}

Custom Platform

Managing dependencies across projects is done via a platform, which can either be a local project or a Maven BOM (AKA a Parent POM). Luckily, Gradle provides nice tooling that will auto-generate a BOM for your project via thejava-platform
plugin.

This requires a separate platform project, as platform projects are disallowed to have actual source code in them:

  • Set up a platform project using the using themaven-plugin andjava-platform plugins in thebuild.gradle.kts file
  • In thedependencies block, use aconstraints block to specify dependencies and versions using theapi()call for desired Maven coordinates.
  • Use themaven-publishing plugin to publish the custom plugin to a Maven repo


  • In the dependent project, specify the newplatform in the dependencies block using theapi() call with Maven coordinates
Example platformbuild.gradle.kts
plugins {
    `java-platform`
    `maven-publish`
}
 
group = "com.stackhawk"
version = "0.0.1"
 
dependencies {
    constraints {
        // Platform declares specific versions of libraries used in subprojects
        api("software.amazon.awssdk:aws-sdk-java:2.10.90")
        api("com.ninja-squad:springmockk:2.0.0")
        api("org.apache.httpcomponents:fluent-hc:4.5.12")
    }
}
 
publishing {
    repositories {
        maven {
            url = uri("http://private-repo")
        }
    }
}
Example dependent projectbuild.gradle.kts dependencies block:
dependencies {
  // Use our custom platform via the Maven POM
  api(platform("com.stackhawk:aerie:0.0.1"))
 
  // Dependencies don't need specific versions, the platform fills those in
  implementation("software.amazon.awssdk:aws-sdk-java")
  testImplementation("com.ninja-squad:springmockk")
  testImplementation("org.apache.httpcomponents:fluent-hc")
}

Blog Banner - Find and Fix Application Security Vulnerabilities with Automated Testing

Summary

This solution fits in nicely with our filesystem-layout-agnostic/multi-repo setup and makes our service’s build files more consistent and easier to manage. Downstream projects simply include the custom plugin and platform and automatically are in line with our dependency versions. Adding the plugin and platform to our build system has added a little overhead, but the trade-offs make it worthwhile.

FEATURED POSTS

What is Cloud API Security? A Complete Guide

Discover essential strategies for cloud API security: Learn about data encryption, authentication mechanisms, and how to combat common threats like injection attacks and broken access control. Get tips on secure coding practices, traffic management, and choosing the right security solutions for your cloud environment.

Security Testing for the Modern Dev Team

See how StackHawk makes web application and API security part of software delivery.

Watch a Demo

StackHawk provides DAST & API Security Testing

Get Omdia analyst’s point-of-view on StackHawk for DAST.

"*" indicates required fields

More Hawksome Posts