Developer guide

Build requirements

  • OpenJDK 21 or its derivative, such as Temurin
    • Consider using a version manager for convenient installation of JDK, such as asdf and jabba.

How to build

We use Gradle to build Armeria. The following command will compile Armeria, run tests and generate JARs:

$ ./gradlew --parallel build

Contributor license agreement

When you are sending a pull request and it's a non-trivial change beyond fixing typos, please sign the ICLA (individual contributor license agreement). Please contact us if you need the CCLA (corporate contributor license agreement).

Setting up your IDE

You can import Armeria into your IDE (IntelliJ IDEA or Eclipse) as a Gradle project.

Before importing the project, run the generateSources task to generate some source files:

$ ./gradlew --parallel -PnoLint generateSources

After importing the project, import the IDE settings as well:

  • settings.jar - See Import settings from a ZIP archive.
  • Make sure to use 'LINE OSS' code style and inspection profile.
    • Go to Preferences > Editors > Code Style and set Scheme option to LINE OSS.
    • Go to Preferences > Editors > Inspections and set Profile option to LINE OSS.

Configure -parameters javac option

You can configure your build tool and IDE to add -parameters javac option. Please refer to Configure -parameters javac option for more information.

Always make the build pass

Make sure your change does not break the build.

  • Run ./gradlew --parallel build locally.
  • It is likely that you'll encounter some Checkstyle or Javadoc errors. Please fix them because otherwise the build will be broken.

All source files must begin with the following copyright header:

Copyright $today.year LY Corporation

LY Corporation licenses this file to you under the Apache License,
version 2.0 (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at:

  https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.

Add Javadoc

All public classes and public or protected methods must have Javadoc, except the classes under com.linecorp.armeria.internal. Referring to an internal API from a public API will trigger build failures.

Check the warnings from the inspection profile

  • Make sure you are using 'LINE OSS' code style and inspection profile.
  • Evaluate all warnings emitted by the 'LINE OSS' inspection profile.
    • Try to fix them all and use the @SuppressWarnings annotation if it's a false positive.

Avoid redundancy

Avoid using redundant keywords. To list a few:

  • final method modifier in a final class
  • static or public modifier in an interface
  • public method modifier in a package-local or private class
  • private constructor modifier in an enum
  • field access prefixed with this. where unnecessary

Use public only when necessary

The classes, methods and fields that are not meant to be used by a user should not be public. Use the most restrictive modifier wherever possible, such as private, package-local and protected, so that static analysis tools can find dead code easily.

Organize

Organize class members carefully for readability, using top-down approach. Although there's no absolute rule of thumb, it's usually like:

  • static fields
  • static methods
  • member fields
  • constructors
  • member methods
  • utility methods (both static and member)
  • inner classes

Check null

Do explicit null-check on the parameters of user-facing public methods. Always use Objects.requireNonNull(Object, String) to do a null-check.

import static java.util.Objects.requireNonNull;

public void setProperty(String name, String value) {
    // Great
    this.name = requireNonNull(name, "name");
    // Not great - we may not know which parameter is null exactly.
    this.name = requireNonNull(name);
    // Not great - too verbose. NPE implies something's null already.
    this.name = requireNonNull(name, "name is null");
    // Not OK
    this.name = name
}

If you are using IntelliJ IDEA and you imported the settings.jar as explained above, try the live template rnn and rnna which will save a lot of time.

Use @Nullable

Use @Nullable annotation for nullable parameters and return types. Do not use @Nonnull annotation since we assume everything is non-null otherwise.

Avoid redundant null checks

Avoid unnecessary null-checks, including the hidden checks in Objects.hashCode() and Objects.equals().

public final class MyClass {
    private final String name;

    public MyClass(String name) {
        // We are sure 'name' is always non-null.
        this.name = requireNonNull(name, "name");
    }

    @Override
    public int hashCode() {
        // OK
        return name.hashCode();
        // Not OK
        return Objects.hash(name);
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        ... usual type check ...
        // OK
        return name.equals(((MyClass) obj).name);
        // Not OK
        return Objects.equals(name, ((MyClass) obj).name);
    }
}

Use meaningful exception messages

When raising an exception, specify meaningful message which gives an explicit clue about what went wrong.

switch (fileType) {
    case TXT: ... break;
    case XML: ... break;
    default:
        // Note that the exception message contains the offending value
        // as well as the expected values.
        throw new IllegalStateException(
                "unsupported file type: " + fileType +
                 " (expected: " + FileType.TXT + " or " + FileType.XML + ')');
}

Validate

Do explicit validation on the parameters of user-facing public methods. When raising an exception, always specify the detailed message in the following format:

public void setValue(int value) {
    if (value < 0) {
        // Note that the exception message contains the offending value
        // as well as the expected value.
        throw new IllegalArgumentException("value: " + value + " (expected: >= 0)");
    }
}

Use Guava's Preconditions if possible

Guava's Preconditions provides checkArgument() and checkState() which can simplify argument or state validation logic:

private boolean someState;
void doSomething(int value) {
    checkArgument(value > 0, "value: %s (expected: > 0)", value);
    checkState(someState, "Cannot be called when ...");
    ...
}

However, you should use a good old if block if it takes an extra job to create an exception message:

if (value <= 0) {
    throw new IllegalArgumentException(
        "value: " + toHumanReadable(value) + " (expected: ...)");
}

Prefer JDK API

Prefer using plain JDK API when the same behavior can be achieved with the same amount of code.

// Prefer A (JDK) - less indirection
Map<String, String> map = new HashMap<>();   // A (JDK)
Map<String, String> map = Maps.newHashMap(); // B (Guava)

// Prefer B (Guava) - simpler yet more efficient
List<String> list = Collections.unmodifiableList(  // A (JDK)
        otherList.stream().filter(...).collect(Collectors.toList()));
List<String> list = otherList.stream().filter(...) // B (Guava)
        .collect(toImmutableList());

Prefer early-return style

Prefer 'early return' code style for readability.

// Great
public void doSomething(String value) {
    if (value == null) {
        return;
    }

    // Do the actual job
}

// Not great
public void doSomething(String value) {
    if (value != null) {
        // Do the actual job
    }
}

However, when the 'normal' execution path is very simple, this may also look beautiful:

@Nullable
public String doSomething(String value) {
    if (value != null) {
        return value.trim();
    } else {
        return null;
    }
}

Prefer MoreObjects.toStringHelper()

Prefer MoreObjects.toStringHelper() to hand-written toString() implementation. However, consider writing hand-written or caching toString() implementation in performance-sensitive places.

Think aesthetics

Do not insert an empty line that hurts code aesthetics.

// OK
if (...) {
    doSomething();
}

// Not OK
if (...) {
    doSomething();
                        // <-- Remove this extra line.
}

Similarly, do not use two or more consecutive empty lines.

// OK
public void a() { ... }

public void b() { ... }

// Not OK
public void a() { ... }

                        // <-- Remove this extra line.
public void b() { ... }

Use JUnit 5 instead of JUnit 4 for testing

We support both JUnit 4 and JUnit 5 for testing, but we recommend to use JUnit 5.

// Imports of JUnit5, Good
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
...

// Imports of JUnit4, Not Good
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
...

Use AssertJ instead of JUnit's assertion API

We prefer AssertJ when writing assertions for test cases.

// Good
assertThat(actualValue).isEqualTo(expectedValue);
assertThatThrownBy(() -> badMethod()).isInstanceOf(IllegalArgumentException.class)
                                     .hasMessageContaining("bad method");
// Not Good
assertEquals(expectedValue, actualValue);
try {
    badMethod();
} catch (IllegalArgumentException e) {
    assertTrue(e.getMessage().contains("bad method"));
}

How to write pull request description

Writing a good pull request description is important to both contributors and reviewers because:

  • it enables a contributor to communicate the intention and context of a pull request more clearly with reviewers.
  • it helps the developers write good release notes.

How much detail should a pull request description have? The general rule of thumb is to put all notable changes in detail. It doesn't have to contain every single tiny detail of the changes. Usually, you need to fill the following 3 sections: Motivation, Modifications and Result.

Motivation

Explain why you're sending the pull request and what problem you're trying to solve. You do not have to include all the detail but please include as much as the reviewers can get the background of this change. If there are related GitHub issues, please leave links to them. If you referred other resources (e.g. RFCs) for making this change, please leave links to them as well.

These are good examples:

Modifications

List the modifications you've made in detail. Again, you do not have to include all the modifications you made but notable changes that you wish the readers know. If the pull request has a deprecation or breaking change, you need to describe it here. For example:

- (Deprecated) Foo.bar() is deprecated.
  - Use Baz.bar() instead.

Result

Specify - Closes #<GitHub issue number> if this resolves the issue. Describe the consequences that a user will face after this pull request is merged.

For example, if the pull request fixes a bug, you can write: You no longer see a Foo exception when using Bar. If you introduce a new feature, you can write: You can now do A using B. These are examples:

  • You no longer see a NullPointerException when a request times out.
  • You can now monitor the state of all live threads and heap using ManagementService.

Add an example snippet if this pull request introduces a new feature, so we can use it in our release notes, e.g.

Server.builder()
      .serviceUnder("/internal/management/", ManagementService.of());

Integrating with Gradle Enterprise

You may want to integrate your local development environment with Gradle Enterprise. By doing so, you may:

  1. Visualize and troubleshoot build issues more easily.
  2. Experience faster builds due to build caches.

Let us know at the Armeria Discord channel, and we'll create an account for you. Afterwards, you may integrate your local environment with the following command:

./gradlew provisionGradleEnterpriseAccessKey