Developer guide
Table of contents
Build requirements
- OpenJDK 21 or its derivative, such as Temurin
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.
- IntelliJ IDEA - See Importing Project from Gradle Model
- Eclipse - Use Buildship Gradle Integration
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 setScheme
option toLINE OSS
. - Go to
Preferences
>Editors
>Inspections
and setProfile
option toLINE OSS
.
- Go to
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.
Add copyright header
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.
- Try to fix them all and use the
Avoid redundancy
Avoid using redundant keywords. To list a few:
final
method modifier in afinal
classstatic
orpublic
modifier in aninterface
public
method modifier in a package-local or private classprivate
constructor modifier in anenum
- 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
fieldsstatic
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:
- Visualize and troubleshoot build issues more easily.
- 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