Language Guide: Java

Last updated
Tags Cloud Server v3.x Server v2.x

This guide will help you get started with a Java application building with Gradle on CircleCI.

Overview

If you’re in a rush, just copy the sample configuration below into a .circleci/config.yml in your project’s root directory and start building.

We are going to make a few assumptions here:

  • You are using Gradle. A Maven version of this guide is available here.
  • You are using Java 11.
  • You are using the Spring Framework. This project was generated using the Spring Initializer.
  • Your application can be distributed as an all-in-one uberjar.

Sample configuration

version: 2
jobs: # a collection of steps
  build:
    # Remove if parallelism is not desired
    parallelism: 2
    environment:
      # Configure the JVM and Gradle to avoid OOM errors
      _JAVA_OPTIONS: "-Xmx3g"
      GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2"
    docker: # run the steps with Docker
      - image: cimg/openjdk:17.0.1 # ...with this image as the primary container; this is where all `steps` will run
        auth:
          username: mydockerhub-user
          password: $DOCKERHUB_PASSWORD  # context / project UI env-var reference
      - image: cimg/postgres:14.0
        auth:
          username: mydockerhub-user
          password: $DOCKERHUB_PASSWORD  # context / project UI env-var reference
        environment:
          POSTGRES_USER: postgres
          POSTGRES_DB: circle_test
    steps: # a collection of executable commands
      - checkout # check out source code to working directory
      # Read about caching dependencies: https://circleci.com/docs/caching/
      - restore_cache:
          key: v1-gradle-wrapper-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
      - restore_cache:
          key: v1-gradle-cache-{{ checksum "build.gradle" }}
      - run:
          name: Run tests in parallel # See: https://circleci.com/docs/parallelism-faster-jobs/
          # Use "./gradlew test" instead if tests are not run in parallel
          command: |
            cd src/test/java
            # Get list of classnames of tests that should run on this node
            CLASSNAMES=$(circleci tests glob "**/*.java" \
              | cut -c 1- | sed 's@/@.@g' \
              | sed 's/.\{5\}$//' \
              | circleci tests split --split-by=timings --timings-type=classname)
            cd ../../..
            # Format the arguments to "./gradlew test"
            GRADLE_ARGS=$(echo $CLASSNAMES | awk '{for (i=1; i<=NF; i++) print "--tests",$i}')
            echo "Prepared arguments for Gradle: $GRADLE_ARGS"
            ./gradlew test $GRADLE_ARGS
      - save_cache:
          paths:
            - ~/.gradle/wrapper
          key: v1-gradle-wrapper-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
      - save_cache:
          paths:
            - ~/.gradle/caches
          key: v1-gradle-cache-{{ checksum "build.gradle" }}
      - store_test_results:
      # Upload test results for display in Test Summary: https://circleci.com/docs/collect-test-data/
          path: build/test-results/test
      - store_artifacts: # Upload test results for display in Artifacts: https://circleci.com/docs/artifacts/
          path: build/test-results/test
      - run:
          name: Assemble JAR
          command: |
            # Skip this for other nodes
            if [ "$CIRCLE_NODE_INDEX" == 0 ]; then
              ./gradlew assemble
            fi
      # As the JAR was only assembled in the first build container, build/libs will be empty in all the other build containers.
      - store_artifacts:
          path: build/libs
      # See https://circleci.com/docs/deployment-integrations/ for deploy examples
workflows:
  version: 2
  workflow:
    jobs:
    - build

Get the code

The configuration above is from a demo Java app, which you can access at https://github.com/CircleCI-Public/circleci-demo-java-spring.

If you want to step through it yourself, you can fork the project on GitHub and download it to your machine. Go to the Projects dashboard in CircleCI and click the Follow Project button next to your project. Finally, delete everything in .circleci/config.yml.

Now we are ready to build a config.yml from scratch.

Config walkthrough

We always start with the version.

version: 2

Next, we have a jobs key. Each job represents a phase in your Build-Test-Deploy process. Our sample app only needs a build job, so everything else is going to live under that key.

version: 2
jobs:
  build:
    # Remove if parallelism is not desired
    parallelism: 2
    environment:
      # Configure the JVM and Gradle to avoid OOM errors
      _JAVA_OPTIONS: "-Xmx3g"
      GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2"

An optional parallelism value of 2 is specified as we would like to run tests in parallel to speed up the job.

We also use the environment key to configure the JVM and Gradle to avoid OOM errors. We disable the Gradle daemon to let the Gradle process terminate after it is done. This helps to conserve memory and reduce the chance of OOM errors.

version: 2
...
    docker:
      - image: cimg/openjdk:17.0.1
        auth:
          username: mydockerhub-user
          password: $DOCKERHUB_PASSWORD  # context / project UI env-var reference
      - image: cimg/postgres:14.0
        auth:
          username: mydockerhub-user
          password: $DOCKERHUB_PASSWORD  # context / project UI env-var reference
        environment:
          POSTGRES_USER: postgres
          POSTGRES_DB: circle_test

We use the CircleCI OpenJDK Convenience images tagged to version 11.0.3-jdk-stretch.

Now we will add several steps within the build job.

We start with checkout so we can operate on the codebase.

Next we pull down the caches for the Gradle wrapper and dependencies, if present. If this is your first run, or if you’ve changed gradle/wrapper/gradle-wrapper.properties and build.gradle, this won’t do anything.

Tip: Dependency caching may not work fully if there are multiple build.gradle files in your project. If this is the case, consider computing a checksum based on the contents of all the build.gradle files, and incorporating it into the cache key.

...
    steps:
      - checkout
      - restore_cache:
          key: v1-gradle-wrapper-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
      - restore_cache:
          key: v1-gradle-cache-{{ checksum "build.gradle" }}

We run ./gradlew test with additional arguments, which will pull down Gradle and/or the project’s dependencies if the cache(s) were empty, and run a subset of tests on each build container. The subset of tests run on each parallel build container is determined with the help of the built-in circleci tests split command.

...
    steps:
      - run:
          name: Run tests in parallel # See: https://circleci.com/docs/parallelism-faster-jobs/
          # Use "./gradlew test" instead if tests are not run in parallel
          command: |
            cd src/test/java
            # Get list of classnames of tests that should run on this node
            CLASSNAMES=$(circleci tests glob "**/*.java" \
              | cut -c 1- | sed 's@/@.@g' \
              | sed 's/.\{5\}$//' \
              | circleci tests split --split-by=timings --timings-type=classname)
            cd ../../..
            # Format the arguments to "./gradlew test"
            GRADLE_ARGS=$(echo $CLASSNAMES | awk '{for (i=1; i<=NF; i++) print "--tests",$i}')
            echo "Prepared arguments for Gradle: $GRADLE_ARGS"
            ./gradlew test $GRADLE_ARGS

Next we use the save_cache step to store the Gradle wrapper and dependencies in order to speed things up for next time.

...
      - save_cache:
          paths:
            - ~/.gradle/wrapper
          key: v1-gradle-wrapper-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
      - save_cache:
          paths:
            - ~/.gradle/caches
          key: v1-gradle-cache-{{ checksum "build.gradle" }}

Next store_test_results uploads the JUnit test metadata from the build/test-results/test directory so that it can show up in the CircleCI dashboard. We also upload the test metadata as artifacts via the store_artifacts in case there is a need to examine them.

...
      - store_test_results:
          path: build/test-results/test
      - store_artifacts:
          path: build/test-results/test

Next we use the ./gradlew assemble command to create an “uberjar” file containing the compiled application along with all its dependencies. We run this only on the first build container instead of on all the build containers running in parallel, as we only need one copy of the uberjar.

We then store the uberjar as an artifact using the store_artifacts step. From there this can be tied into a continuous deployment scheme of your choice.

...
      - run:
          name: Assemble JAR
          command: |
            # Skip this for other nodes
            if [ "$CIRCLE_NODE_INDEX" == 0 ]; then
              ./gradlew assemble
            fi
      # As the JAR was only assembled in the first build container, build/libs will be empty in all the other build containers.
      - store_artifacts:
          path: build/libs

Lastly, we define a workflow named workflow which the build job will execute as the only job in the workflow.

...
workflows:
  version: 2
  workflow:
    jobs:
    - build

Nice! You just set up CircleCI for a Java app using Gradle and Spring.

See also



Help make this document better

This guide, as well as the rest of our docs, are open source and available on GitHub. We welcome your contributions.

Need support?

Our support engineers are available to help with service issues, billing, or account related questions, and can help troubleshoot build configurations. Contact our support engineers by opening a ticket.

You can also visit our support site to find support articles, community forums, and training resources.