Java client library¶
The Java client library is the first class client implementation that provides the access to all operations exposed by a Central Dogma server.
Tip
Keep the API documentation open in another browser tab. You’ll find it to be a very useful companion.
Adding centraldogma-client
as a dependency¶
Gradle:
dependencies { compile 'com.linecorp.centraldogma:centraldogma-client-armeria:0.72.0' }
Maven:
<dependencies> <dependency> <groupId>com.linecorp.centraldogma</groupId> <artifactId>centraldogma-client-armeria</artifactId> <version>0.72.0</version> </dependency> </dependencies>
Creating a client¶
First, we should create a new instance of CentralDogma
:
import com.linecorp.centraldogma.client.CentralDogma;
import com.linecorp.centraldogma.client.armeria.ArmeriaCentralDogmaBuilder;
// The default port 36462 is used if unspecified.
CentralDogma dogma = new ArmeriaCentralDogmaBuilder()
.host("127.0.0.1")
.build();
// You can specify an alternative port or enable TLS as well:
CentralDogma dogma2 = new ArmeriaCentralDogmaBuilder()
.useTls() // Enable TLS.
.host("example.com", 8443); // Use port 8443.
.build();
Note
Internally, the client uses Armeria as its networking layer. You may want to customize the client
settings, such as specifying alternative Armeria ClientFactory
or configuring Armeria ClientBuilder
.
Specifying an access token¶
You must specify an access token if your Central Dogma server has enabled authorization.
CentralDogma dogma = new ArmeriaCentralDogmaBuilder()
.host("127.0.0.1")
.accessToken("appToken-cffed349-d573-457f-8f74-4727ad9341ce")
.build();
Note
See Authentication and Access Control for more information about securing a Central Dogma server and managing permissions and access tokens.
Getting a file¶
Once a client is created, you can get a file from a repository:
import java.util.concurrent.CompletableFuture;
import com.linecorp.centraldogma.common.Entry;
import com.linecorp.centraldogma.common.EntryType;
import com.linecorp.centraldogma.common.Revision;
import com.linecorp.centraldogma.common.Query;
CentralDogma dogma = ...;
CompletableFuture<Entry<String>> future =
dogma.getFile("myProj", "myRepo", Revision.HEAD, Query.ofText("/a.txt"));
Entry<String> entry = future.join();
assert entry.type() == EntryType.TEXT
assert entry.content() instanceof String; // Text file's content type is String.
System.err.println(entry.content());
The getFile()
call above will fetch the latest revision of /a.txt
because we specified Revision.HEAD
which is equal to new Revision(-1)
. If you want to fetch a specific revision, you can specify the revision
you desire. e.g. new Revision(42)
or new Revision(-7)
Note
Not sure what the meaning of a negative revision number is? Read Concepts.
Note that we used Query.ofText()
, which tells Central Dogma to fetch the textual content. For a JSON file,
you need to use Query.ofJson()
:
import com.fasterxml.jackson.databind.JsonNode;
CentralDogma dogma = ...;
CompletableFuture<Entry<JsonNode>> future =
dogma.getFile("myProj", "myRepo", Revision.HEAD, Query.ofJson("/b.json"));
Did you notice the return type changed slightly? The type parameter of Entry
is not String
anymore but
JsonNode
(from Jackson), because we know we are fetching a JSON
file.
Alternatively, you can use Query.ofJsonPath()
to retrieve the result of JSON path evaluation instead of
the whole content, which would be useful especially when you are interested only in a certain part of a
large JSON file:
CentralDogma dogma = ...;
CompletableFuture<Entry<JsonNode>> future =
dogma.getFile("myProj", "myRepo", Revision.HEAD,
Query.ofJsonPath("/b.json", "$.someValue"));
Central Dogma server will apply the JSON path expression $.someValue
to the content of /b.json
and return the query result to the client. For example, if /b.json
contains the following:
{ "someValue": 42, "otherValue": "foo" }
You would get:
42
Note
Central Dogma uses Jayway’s JSON path implementation. Refer to their project page for syntax, example and the list of supported functions.
Getting a merged file¶
You can get a merged file from a repository:
import com.linecorp.centraldogma.common.MergeQuery;
import com.linecorp.centraldogma.common.MergeSource;
CentralDogma dogma = ...;
List<MergeSource> mergeSources = Arrays.asList(MergeSource.ofRequired("/a.json"),
MergeSource.ofRequired("/b.json"),
MergeSource.ofRequired("/c.json"));
CompletableFuture<MergedEntry<JsonNode>> future =
dogma.mergeFiles("myProj", "myRepo", Revision.HEAD,
MergeQuery.ofJson(mergeSources));
MergedEntry<JsonNode> mergedEntry = future.join();
assert mergedEntry.type() == EntryType.JSON
assert mergedEntry.content() instanceof JsonNode;
System.err.println(mergedEntry.content());
The mergeFiles()
call above will retrieve the MergedEntry
which contains a JSON document which
is the result of merging the files specified in the MergeQuery
sequentially.
We specified Revision.HEAD
, so the latest revision of /a.json
, /b.json
and /c.json
will be merged. If you want to fetch at the specific revision, you can specify the revision as we
did in Getting a file.
Only merging JSON files is currently supported. The merge happens traversing children in the JSON object
recursively. In the merge process, the value is simply replaced by the value who has same property name.
Let’s consider that the contents of the /a.json
, /b.json
and /c.json
are as follows:
/a.json
{
"someObject": {
"nullInSomeObject": null
},
"someValue": "foo"
}
/b.json
{
"someObject": {
"booleanInSomeObject": true // Add this field because it it not in "/a.json".
},
"someValue": "bar" // Replace the value with "bar".
}
/c.json
{
"someObject": {
// Replace the null with 100. null can be converted to any type.
"nullInSomeObject": 100
}
}
Then, the content of the merged entry will be:
{
"someObject": {
"nullInSomeObject": 100,
"booleanInSomeObject": true
},
"someValue": "bar"
}
Note
Corresponding types of values should be same or one of the types must be null
to replace.
If their types do not match or neither value is null
, you will get a QueryExecutionException
.
You can mark some files involved in the merge process as optional.
CentralDogma dogma = ...;
List<MergeSource> mergeSources = Arrays.asList(MergeSource.ofRequired("/a.json"),
MergeSource.ofOptional("/b.json"), // <-- It's optional!
MergeSource.ofRequired("/c.json"));
CompletableFuture<MergedEntry<JsonNode>> future =
dogma.mergeFiles("myProj", "myRepo", Revision.HEAD,
MergeQuery.ofJson(mergeSources));
Note that we used MergeSource.ofOptional("/b.json")
, which tells to include the /b.json
file only if it
exists in the repository. If it does not exist, /a.json
and /c.json
will be merged sequentially.
The files specified as required must exist in the repository. You will get an EntryNotFoundException
otherwise.
You will get the EntryNotFoundException
as well when you specify all of the files as optional
and none of them exists.
As we used Query.ofJsonPath()
in Getting a file, you can use MergeQuery.ofJsonPath()
to
retrieve the result of JSON path evaluation of the MergedEntry
.
CentralDogma dogma = ...;
List<MergeSource> mergeSources = Arrays.asList(MergeSource.ofRequired("/a.json"),
MergeSource.ofOptional("/b.json"),
MergeSource.ofRequired("/c.json"));
CompletableFuture<MergedEntry<JsonNode>> future =
dogma.mergeFiles("myProj", "myRepo", Revision.HEAD,
MergeQuery.ofJsonPath(mergeSources, "$.someValue"));
Central Dogma server will apply the JSON path expression $.someValue
to the content of the
MergedEntry
, and return the query result to the client.
Pushing a commit¶
You can also push a commit into a repository programmatically:
import com.linecorp.centraldogma.common.Change;
import com.linecorp.centraldogma.common.Commit;
CentralDogma dogma = ...;
CompletableFuture<Commit> future =
dogma.push("myProj", "myRepo", Revision.HEAD,
"Add /c.json and remove /b.json",
Change.ofUpsert("/c.json", "{ \"foo\": \"bar\" }"),
Change.ofRemoval("/b.json"));
Commit commit = future.join();
System.err.printf("Pushed a commit %s at %s%n",
commit.revision(), commit.whenAsText());
In this example, we pushed a commit that contains two changes: one that adds /c.json
and the other that
removes /b.json
.
Note that we specified Revision.HEAD
as the base revision. It means this commit is against the latest
commit in the repository myRepo
. Alternatively, you can specify an absolute revision so that you are
absolutely sure that nobody pushed a commit while you prepare yours: (pun intended 😉)
import java.util.concurrent.CompletionException;
CentralDogma dogma = ...;
CompletableFuture<Commit> future = dogma.push(..., new Revision(3), ...);
try {
future.join();
} catch (CompletionException e) {
Throwable cause = e.getCause();
if (cause instanceof ChangeConflictException) {
// Somebody pushed a commit newer than revision 3 or
// our changes cannot be applied to the revision 3 cleanly.
}
}
Watching a file¶
Some configuration properties are dynamic. They are changed often and they must be applied without restarting the process. The client library provides an easy way to watch a file:
import com.linecorp.centraldogma.client.Latest;
import com.linecorp.centraldogma.client.Watcher;
CentralDogma dogma = ...;
Watcher<JsonNode> watcher =
dogma.fileWatcher("myProj", "myRepo",
Query.ofJsonPath("/some_file.json", "$.foo"));
// Register a callback for changes.
watcher.watch((revision, value) -> {
System.err.printf("Updated to %s at %s%n", value, revision);
});
// Alternatively, without using a callback:
Latest<JsonNode> latest = watcher.awaitInitialValue(); // Wait for the initial value.
System.err.printf("Initial: %s at %s%n", latest.value(), latest.revision());
You would want to register a callback to the Watcher
or check the return value of Watcher.latest()
periodically to apply the new settings to your application.
Specifying multiple hosts¶
You can also specify more than one host using the host()
method:
import com.linecorp.centraldogma.client.armeria.ArmeriaCentralDogmaBuilder;
ArmeriaCentralDogmaBuilder builder = new ArmeriaCentralDogmaBuilder();
// The default port 36462 is used if unspecified.
builder.host("replica1.example.com");
// You can specify an alternative port number.
builder.host("replica2.example.com", 1234);
CentralDogma dogma = builder.build();
Using client profiles¶
You can load the list of the Central Dogma servers from one of the following JSON files in the class path using
ArmeriaCentralDogmaBuilder.profile(String...)
:
centraldogma-profiles-test.json
centraldogma-profiles.json
(ifcentraldogma-profiles-test.json
is missing)
ArmeriaCentralDogmaBuilder builder = new ArmeriaCentralDogmaBuilder();
// Loads the profile 'beta' from:
// - /centraldogma-profiles-test.json or
// - /centraldogma-profiles.json
builder.profile("beta");
CentralDogma dogma = builder.build();
The following example centraldogma-profiles.json
contains two profiles, beta
and release
, and
they contain two replicas, replica{1,2}.beta.example.com
and replica{1,2}.release.example.com
respectively. The replicas in the release
profile support both http
and https
whereas
the replicas in the beta
profile support http
only:
[ {
"name": "beta",
"priority": 0,
"hosts": [ {
"host": "replica1.beta.example.com",
"protocol": "http",
"port": 36462
}, {
"host": "replica2.beta.example.com",
"protocol": "http",
"port": 36462
} ]
}, {
"name": "release",
"priority": 0,
"hosts": [ {
"host": "replica1.release.example.com",
"protocol": "http",
"port": 36462
}, {
"host": "replica1.release.example.com",
"protocol": "https",
"port": 8443
}, {
"host": "replica2.release.example.com",
"protocol": "http",
"port": 36462
}, {
"host": "replica2.release.example.com",
"protocol": "https",
"port": 8443
} ]
} ]
Tip
Use the JSON schema to validate your
centraldogma-profiles.json
file.
You may want to archive this file into a JAR file and distribute it as the official client profiles via
a Maven repository, so that your users get the up-to-date host list easily. For example, a user could put
centraldogma-profiles-1.0.jar
into his or her class path:
$ cat centraldogma-profiles.json
[ { "name": "beta", "priority": 0, "hosts": [ ... ] },
{ "name": "release", "priority": 0, "hosts": [ ... ] } ]
$ jar cvf centraldogma-profiles-1.0.jar centraldogma-profiles.json
added manifest
adding: centraldogma-profiles.json
Custom client profiles¶
A user can add his or her own custom client profiles other than the official ones by adding more
centraldogma-profiles.json
files to the class path. The following example adds a custom profile called
localtest
:
[ {
"name": "localtest",
"hosts": [ {
"host": "127.0.0.1",
"protocol": "http",
"port": 36462
} ]
} ]
A user can also override the official profile provided by an administrator by specifying a higher priority.
For example, you can override the beta
profile using priority 100
which is higher than the default
priority of 0
:
[ {
"name": "beta",
"priority": 100,
"hosts": [ {
"host": "replica1.alternative-beta.example.com",
"protocol": "http",
"port": 36462
}, {
"host": "replica2.alternative-beta.example.com",
"protocol": "http",
"port": 36462
} ]
} ]
Note that other profiles such as release
are still loaded from the centraldogma-profiles.json
distributed by
the administrator.
Using DNS-based lookup¶
Central Dogma Java client always retrieves all the IP addresses of a host from the current system DNS server or
the /etc/host
file. Instead of specifying all the individual replica addresses in a client profile,
consider specifying a single host name that’s very unlikely to change in the client profile and add multiple
A
or AAAA
DNS records to the host name:
$ cat centraldogma-profiles.json
[ {
"name": "release",
"hosts": [ {
"host": "all.dogma.example.com",
"protocol": "http",
"port": 36462
} ]
} ]
$ dig all.dogma.example.com
; <<>> DiG 9.12.1-P2 <<>> all.dogma.example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58779
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1440
;; QUESTION SECTION:
;all.dogma.example.com. IN A
;; ANSWER SECTION:
all.dogma.example.com. 300 IN A 192.168.1.1
all.dogma.example.com. 300 IN A 192.168.1.2
all.dogma.example.com. 300 IN A 192.168.1.3
;; Query time: 54 msec
The client will periodically send DNS queries respecting the TTL values advertised by the DNS server and update the endpoint list dynamically, so that an administrator can add or remove a replica without distributing a new client profile JAR again.
Spring Boot integration¶
If you are using Spring Framework, you can inject CentralDogma
client very easily. First, add centraldogma-client-spring-boot3-starter
into your dependencies.
Tip
Use the centraldogma-client-spring-boot2-starter
dependency if you are using Spring Boot 2 or running your application with
a Java version lower than 17.
Gradle:
dependencies { compile 'com.linecorp.centraldogma:centraldogma-client-spring-boot3-starter:0.72.0' }
Maven:
<dependencies> <dependency> <groupId>com.linecorp.centraldogma</groupId> <artifactId>centraldogma-client-spring-boot3-starter</artifactId> <version>0.72.0</version> </dependency> </dependencies>
Then, add a new section called centraldogma
to your Spring Boot application configuration, which is often
named application.yml
:
centraldogma:
hosts:
- replica1.example.com:36462
- replica2.example.com:36462
- replica3.example.com:36462
access-token: appToken-cffed349-d573-457f-8f74-4727ad9341ce
If you prefer using client profiles as described in Using client profiles, use the profile
property:
centraldogma:
profile: beta
access-token: appToken-cffed349-d573-457f-8f74-4727ad9341ce
If neither hosts
nor profile
property is specified, currently active
Spring Boot profile
will be used as the client profile. When more than one Spring Boot profile are active, the last matching one
will be chosen.
Note
Do not confuse ‘Central Dogma client profile’ with ‘Spring Boot profile’.
You can also enable a TLS connection or override the default health check request interval:
centraldogma:
profile: staging
access-token: appToken-cffed349-d573-457f-8f74-4727ad9341ce
use-tls: true
health-check-interval-millis: 15000
Once configured correctly, a new CentralDogma
client will be created and
injected into your application like the following:
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.linecorp.centraldogma.client.CentralDogma;
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
// CentralDogma is injected automatically by CentralDogmaConfiguration.
@Bean
public CommandLineRunner commandLineRunner(CentralDogma dogma) {
return args -> {
System.err.println(dogma.listProjects().join());
};
}
}
Read the Javadoc¶
Refer to the API documentation of ‘CentralDogma’ interface for the complete list of operations you can perform with a Central Dogma server, which should be definitely much more than what this tutorial covers, such as fetching and watching multiple files.