# Java

> Write custom APIs, hooks, schedulers and event listeners of API Maker in Java. static Object main(G g) in a class, g.req, g.res, g.sys.db, am.Json, AmErrors, packages from Maven Central, with many examples.

Source: https://docs.apimaker.dev/v4/docs/languages/java.html

Code in Java is a class with `static Object main(G g)`. It is compiled once by the compiler of the JDK, and runs on the image of the JDK of Eclipse Temurin (`eclipse-temurin:25-jdk` unless the [settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) name another version), with only the packages you add from Maven Central.

> **In short**
>
> - Write a class with `static Object main(G g)`.
> - It is compiled once, and runs on `eclipse-temurin:25-jdk` unless the settings name another version.
> - Refuse a request with `throw new AmErrors(message, 400)`.

**hello.java**

```java
import am.G;
import java.util.Map;

class Api {
    static Object main(G g) {
        String name = g.req.getBody().getString("name", "world");
        return Map.of("hello", name);
    }
}
```

| | |
|---|---|
| Runtime | JDK 20 or later, a version of `eclipse-temurin:<version>-jdk` : `25` (default), `21` |
| `main` | a static method `main(G g)` of any class : it returns `Object`, any other type, or is `void` |
| Class | `class Api` in a new item, not `public` : its file is named after the item (`orders.java`), and Java wants a public class in a file of its name. Every piece of code is compiled on its own, so each can have its own `Api`, in a sandbox and in [git](https://docs.apimaker.dev/v4/docs/Git/git.html#project-of-every-language) |
| Answer | what `main` returns, written as JSON; `null` lets the API go on |
| Calls of `g.sys` | typed : what a call takes is a class made key after key (`new GetAll("mysql_8", "inventory", "customers").limit(10)`), what it answers has its type (`List<JsonObject>`, `JsonObject`, `long`, `UpdateManyResult`) |
| Values | plain JSON : an object is an `am.JsonObject` (a `Map<String, Object>` which reads its keys by their types : `getString`, `getLong`, `getObject`…), then `List<Object>`, `String`, `Long` or `Double`, `Boolean`, `null` |
| Imports | `am.G`, `am.GetAll`, `am.JsonObject`, `am.AmErrors`… : import what the code names, as in any Java file |
| Packages | Maven Central : `com.google.code.gson:gson` `2.11.0` |
| Printing | `System.out.println` goes to the console of the account |

## The request and the answer

`g.req` and `g.res` have a getter and a setter for each key they are known to have : `g.req.getBody()`, `g.req.getQuery()`, `g.req.getParams()` and `g.req.getHeaders()` give the objects of the request, `g.res.setStatusCode(201)` sets the status of the answer. Each object is a `JsonObject`, which reads a key by its type without a cast : `g.req.getBody().getString("name")` is `null` when the key is not there, `getString("name", "world")` gives a value for that case.

A body which is a list (`[{…}, {…}]`, several rows to save) is read with `g.req.getList("body")` ; `g.req.getBody()` is an empty `JsonObject` then.

**Read the request, shape the answer**

```java
import am.G;
import am.JsonObject;

class Api {
    static Object main(G g) {
        String status = g.req.getQuery().getString("status", "open");
        boolean signed = g.req.getHeaders().has("x-am-authorization");
        g.res.setStatusCode(201);
        g.res.getHeaders().put("X-Request-Kind", "orders");
        return JsonObject.of("customer", g.req.getParams().get("id"), "status", status, "signed", signed);
    }
}
```

- `JsonObject.of("key", value, …)` makes an object from keys and values in turn (a value can be `null`) ; `Map.of(…)` and any other `Map` are objects too. A `JsonObject` reads a key as what you expect : `getString`, `getLong`, `getInt`, `getDouble`, `getBoolean` (`0`, `false` or the value you give when something else is there), `getObject` (an empty one when none is there), `getList`, `getObjects`, `getStrings`, and `at("address", "phones", 0, "number")` for a path.
- A whole number of the request is a `Long`, any other number a `Double` : read numbers with `getLong` and `getDouble`, or as a `Number`, not with a cast to `Integer`.
- A `Map`, a `List`, an array (a `byte[]` is bytes : see below), a record (its components), an object of a class of yours (its fields), an `Optional`, an enum (its name) : all are answers. `NaN` and infinities are `null`; a `java.util.Date` and the types of `java.time` are texts (`2026-01-02T03:04:05.678Z`).
- What you change of `g.req` in a pre hook is what the API sees.

### Bytes as the answer

A file, a picture, a PDF : return its bytes (a `byte[]`), or give them to `g.res.setOutput`, and name their kind of content. The answer is those bytes as they are, whatever the kind of content, with the headers of the code. (A text under a kind of bytes, `image/gif`, is read as base64.)

**A picture as the answer**

```java
import am.G;
import java.util.Base64;

class Api {
    static Object main(G g) {
        byte[] picture = Base64.getDecoder().decode("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=");
        g.res.setContentType("image/gif");
        g.res.getHeaders().put("Content-Disposition", "inline; filename=\"pixel.gif\"");
        return picture;
    }
}
```

## Calling API Maker : `g.sys`

Each method is a call of the server, with its types. What it takes is a class of `am` made with what it is about, then key after key (`GetAll`, `GetById`, `Count`…) ; what it answers is typed too : rows are a `List<JsonObject>`, a row a `JsonObject` (`null` when there is none), a count a `long`, what an API describes a class with a getter for each key (`UpdateManyResult`). The thread of the request waits for the answer; the other requests go on.

**Database calls**

```java
import am.Count;
import am.G;
import am.GetAll;
import am.GetById;
import am.JsonObject;
import java.util.List;
import java.util.Map;

class Api {
    static Object main(G g) {
        List<JsonObject> rows = g.sys.db.getAll(new GetAll("mysql_8", "inventory", "customers").limit(10).sort("-customer_id"));
        JsonObject two = g.sys.db.getById(new GetById("mysql_8", "inventory", "customers", 2));
        long count = g.sys.db.count(new Count("mysql_8", "inventory", "customers").find(Map.of("country", "IN")));
        return JsonObject.of("first", rows.isEmpty() ? null : rows.get(0), "name", two.getString("first_name"), "inIndia", count);
    }
}
```

**Save, update, remove**

```java
import am.G;
import am.JsonObject;
import am.RemoveById;
import am.Save;
import am.Saved;
import am.UpdateById;
import java.util.Map;

class Api {
    static Object main(G g) {
        Saved saved = g.sys.db.saveSingleOrMultiple(new Save("mysql_8", "inventory", "customers", JsonObject.of("first_name", "Ada", "last_name", "Lovelace")));
        g.sys.db.updateById(new UpdateById("mysql_8", "inventory", "customers", 2).updateData(Map.of("first_name", "MALLORY")));
        g.sys.db.removeById(new RemoveById("mysql_8", "inventory", "customers", 101));
        // What a save answers : saved.row(), saved.rows(), saved.isList().
        return saved.row();
    }
}
```

- A key the class has no method for is set as it is : `new GetAll("mysql_8", "inventory", "customers").with("cursor", "abc")`. A class is made of a map too : `new GetAll(g.req.getBody())`.
- The generated APIs are under `g.sys.db.gen` (`getAllGen`…). The whole answer of a call (`isSuccess()`, `getStatusCode()`, `getData()`, `getTotalCount()`, `getErrors()`) : `g.sys.db.full.getAll(…)`, an `ApiResponse<List<JsonObject>>`.

Calls at the same time are threads. Virtual threads (JDK 21 and later) cost next to nothing :

**Calls at the same time**

```java
import am.Count;
import am.G;
import am.JsonObject;
import java.util.concurrent.Executors;

class Api {
    static Object main(G g) throws Exception {
        try (var threads = Executors.newVirtualThreadPerTaskExecutor()) {
            var orders = threads.submit(() -> g.sys.db.count(new Count("mongodb", "shop", "orders")));
            var products = threads.submit(() -> g.sys.db.count(new Count("mongodb", "shop", "products")));
            return JsonObject.of("orders", orders.get(), "products", products.get());
        }
    }
}
```

- `main` can say `throws Exception`. A call which fails in a thread comes out of `get()` : its errors are the errors of the answer.
- On JDK 20 use `Executors.newCachedThreadPool()` : virtual threads are part of the JDK from 21 on.

### Rows as they arrive

**Sum a large table**

```java
import am.G;
import am.GetAll;
import java.util.Map;

class Api {
    static Object main(G g) {
        double[] total = { 0 };
        g.sys.db.getAllByStream(new GetAll("mysql_8", "inventory", "orders"), row -> total[0] += row.getDouble("amount"));
        return Map.of("amount", total[0]);
    }
}
```

The function of the rows is an `am.Rows` (`row -> …`), and each row a `JsonObject`. A lambda of Java does not assign a local variable : keep the sum in an array of one, or in a `DoubleAdder`. What the function throws ends the call.

### System APIs and cache

**Encrypt, hash, events and cache keys**

```java
import am.G;
import am.JsonObject;
import java.util.List;
import java.util.Map;

class Api {
    static Object main(G g) {
        String sealedData = g.sys.system.encrypt(Map.of("card", "4111"));
        String digest = g.sys.system.hash("my text");
        g.sys.system.emitEvent("order_created", Map.of("id", 7), List.of("notify_shop"));
        g.sys.cache.setKey("last_order", "7", 600);
        return JsonObject.of("sealedData", sealedData, "digest", digest, "last", g.sys.cache.getKey("last_order"));
    }
}
```

## Errors

**Refuse a request**

```java
import am.AmErrors;
import am.G;
import java.util.Map;

class Api {
    static Object main(G g) {
        if (!g.req.getBody().has("quantity")) throw new AmErrors("'quantity' is missing.", 400);
        return Map.of("ok", true);
    }
}
```

**Catch the errors of a call**

```java
import am.AmErrors;
import am.G;
import am.RemoveById;
import java.util.Map;

class Api {
    static Object main(G g) {
        try {
            g.sys.db.removeById(new RemoveById("mysql_8", "inventory", "customers", 404));
            return Map.of("removed", true);
        } catch (AmErrors refused) {
            g.logger.warn("not removed :", refused.getMessage());
            return Map.of("removed", false, "code", refused.code);
        }
    }
}
```

- `AmErrors` is a `RuntimeException` : nothing has to be declared. `refused.errors` is the list of the errors (each a `ResponseError` : `getMessage()`, `getCode()`, `getField()`…), `refused.code` the code of the first one. Several errors at once : `throw new AmErrors(List.of(Map.of("code", 400, "message", "…"), …))`.
- Any other exception answers `500` with its type and message, the message of what it happened because of, and the lines of your code : `at Api.helper(orders.java:7)`.

## Logs, printing and shared values

**Logs and shared values**

```java
import am.G;
import java.util.Map;

class Api {
    static Object main(G g) {
        g.logger.log("order", 7, "paid");                 // "order 7 paid"
        g.logger.error(Map.of("step", "payment"));        // maps, lists and records as indented JSON
        System.out.println("kept in the console of the account");
        // Lives as long as the runner, shared by every piece of code it runs.
        return g.shared.merge("calls", 1, (old, one) -> (Integer) old + 1);
    }
}
```

## Hooks

**Pre hook : only my rows**

```java
import am.G;

class Api {
    static Object main(G g) {
        // objectAt : the object at a key, made and kept there when none is.
        g.req.getQuery().objectAt("find").put("owner_id", g.req.getAuth().getObject("authAMDB").get("id"));
        return null;
    }
}
```

**Post hook : hide a column**

```java
import am.G;
import java.util.List;
import java.util.Map;

class Api {
    static Object main(G g) {
        if (g.res.getOutput() instanceof List<?> rows) {
            for (Object row : rows) {
                if (row instanceof Map<?, ?> object) object.remove("password");
            }
        }
        return null;
    }
}
```

## Utility classes

A [utility class](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) written in Java is a class of its name. The utility classes of Java of an account are compiled together, and the other Java code of the account names what they declare, without an import.

**The utility class Tax**

```java
public class Tax {
    public static final double RATE = 0.2;

    public static double add(double amount) {
        return Round.cents(amount * (1 + RATE));   // Round : another utility class
    }
}
```

**A custom API which uses it**

```java
import am.G;
import java.util.Map;

class Api {
    static Object main(G g) {
        return Map.of("total", Tax.add(g.req.getBody().getDouble("amount")));
    }
}
```

- The class has the name of the utility class (`public class Tax` of the class `Tax`) : letters of a to z, digits and `_`, no digit first, no word of Java (`record`, `class`…) and not `am`. One class of a name per account, whatever its folder : the classes are in no package, so write no `package` line.
- The classes are loaded once per runner and shared by all Java code of the account there : what a class keeps in a static field is kept there. A class which is saved again is compiled anew with the others, and the code which uses them is compiled again on its next run.
- A class of the code comes before a utility class of the same name : every piece of code keeps its own `Api`.
- What does not compile in a class is said with its file and its line : `src/utility_classes/utils---Tax/Tax.java:3: error: cannot find symbol…`.
- In the Git repository the class is `src/utility_classes/<folder>---<name>/<name>.java` : the project of Gradle compiles the classes together, then every piece of code with them.

## Test cases

A [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#other-languages) written in Java has a method per test : every static method whose name starts with `test` and which takes `G g`, in any class. What it throws is its failure.

**A test case**

```java
import am.G;
import am.JsonObject;
import am.Mock;
import am.RunCustomApi;
import java.util.List;
import java.util.Map;

class ShippingTests {
    static void testTheCostOfAParcel(G g) {
        g.sys.test.mock(new Mock("db.getAll").when(Map.of("collection", "rates")).returns(List.of(Map.of("per_kg", 2.5))));
        JsonObject output = JsonObject.from(g.sys.test.runCustomApi(new RunCustomApi("shippingCost").body(Map.of("weight", 4))));
        if (output.getDouble("cost") != 10) throw new IllegalStateException("the cost is " + output.get("cost"));
    }
}
```

- `g.sys.test` : `runCustomApi(new RunCustomApi(…))`, `runCustomApiFull(…)` for the whole answer (`getData()`, `getStatusCode()`, `getHeaders()`, `getLogs()`), `mock(new Mock(…))`, `mocks(List<Mock>)`, `clearMocks()` or `clearMocks(method)`, `calls()` or `calls(method)`.
- The tests run in the order they are written. The custom API under test can be written in any language.
- Throw to fail : the `assert` of Java is checked only by a JVM started with `-ea`, and the runner is not.
- The lines of a test run are counted on JDK 24 and later. On an older JDK the tests run as well, and the coverage has no lines of Java.

## Packages

Add `com.google.code.gson:gson` in [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages), then :

**A package of Maven Central**

```java
import am.G;
import com.google.gson.Gson;
import java.util.Map;

class Api {
    static Object main(G g) {
        return Map.of("json", new Gson().toJson(java.util.List.of(1, 2)));
    }
}
```

- A package is named by its group and its artifact : `group:artifact`. Versions are the ones of Maven : `2.11.0`, `33.4.0-jre`, a range `[1.0,2.0)`; empty is the latest release.
- Maven resolves the packages, with what they need, when the image is built; while code runs nothing is downloaded. Code which imports what is not there is told so : `'com.google.gson' is not there : is its package in the packages of Java in the settings of the sandbox?`.
- [Kotlin](https://docs.apimaker.dev/v4/docs/languages/kotlin.html) has packages of its own, from Maven Central too : a package added to Java is not there for Kotlin code.

## How it runs

- Each piece of code is compiled once, with its lines : stack traces say them. Saving starts that compilation in the background, so the first request does not wait for it, and code which does not compile is reported in the console of the account (`main.java:3: error: ';' expected`).
- Every request runs on a thread of its own; requests run at the same time, on every core.
- Static fields of your code live as long as the runner, per piece of code : protect them (`AtomicLong`, `synchronized`), or use `g.shared`.
- Each piece of code has a class loader of its own : two pieces of code never see each other's classes, only the utility classes of the account.

## In the code editor

The editor completes `g` as Java writes it, the names of your instances, databases and tables, and the columns of a table on the rows a call answers. More in [the code editor in every language](https://docs.apimaker.dev/v4/docs/languages/index.html#editor).

**Where the editor helps**

```java
import am.G;
import am.GetAll;
import am.JsonObject;
import java.util.List;

class Api {
    static Object main(G g) {
        // g.  →  req, res, sys, logger, shared      g.sys.db.  →  getAll, getById, query, gen, full…
        g.logger.log(g.req.getBody());  // g.req.  →  getBody, getQuery, getHeaders, getAuth…      g.res.  →  setStatusCode, getHeaders…
        List<JsonObject> rows = g.sys.db.getAll(new GetAll("mysql_8", "inventory", "customers").limit(10));
        //                      new GetAll("  →  your instances, then its databases, then their tables
        //                      new GetAll(…).  →  find, limit, sort, select…
        for (JsonObject row : rows) {
            g.logger.log(row.getString("first_name"));  // row.getString("  →  the columns of customers
        }
        return rows.get(0).getString("first_name");
    }
}
```

- After `new GetAll(…).` the editor offers the methods which set its keys, each with what it is for ; a method gives the same value, so the next key follows. Inside `.find(JsonObject.of("` (or `Map.of("`) it offers the columns of the table, inside `.select("` and `.sort("` too.
- What a call answers has its type : on the rows of a `getAll` the columns are offered inside the quotes of `getString`, `getLong`, `getDouble`, `getObject`… and of `get` (`row.get("`), in a `for` loop and in a lambda (`rows.forEach(row -> …)`). `changed.` (an `updateMany`) offers `getUpdatedRowsCount`, `saved.` offers `row`, `rows` and `isList`, the whole answer of `g.sys.db.full` offers `isSuccess`, `getData`, `getErrors`…
- `g.req.` and `g.res.` offer a getter and a setter for each key of the request and of the answer ; `g.req.getAuth().` the opened tokens, written `g.req.getAuth().get("authAMUser")`.
- A value your code says is an object has its keys and what a `JsonObject` has : `JsonObject.from(value).` and `((JsonObject) value).`.

## Reference

| `g.` | Java |
|---|---|
| `req` | `am.Request` : `getBody()`, `getQuery()`, `getParams()`, `getHeaders()`, `getAuth()`, `getReqInfo()` (each a `JsonObject`), `getEventData()`, `isApiRequestFromUser()`, and a setter for each ; a `JsonObject` itself |
| `res` | `am.Response` : `setStatusCode`, `setContentType`, `getHeaders()`, `getShared()`, `getOutput()`, `setOutput`, `getErrors()`, `getWarnings()` ; a `JsonObject` itself |
| `sys.db` | `getAll(GetAll)`, `getById(GetById)`, `query(Query)`, `saveSingleOrMultiple(Save)`, `masterSave(Save)`, `arrayOperations(ArrayOperations)`, `updateById(UpdateById)`, `updateMany(UpdateMany)`, `replaceById(UpdateById)`, `removeById(RemoveById)`, `removeByQuery(RemoveByQuery)`, `aggregate(Aggregate)`, `count(Count)`, `distinct(Distinct)`, `distinctQuery(Distinct)`, `getAllByStream`, `queryByStream` ; `sys.db.gen.getAllGen`… |
| `sys.db.full`, `sys.db.gen.full`, `sys.system.full`, `sys.cache.full` | the same calls, each with its whole answer : `ApiResponse<T>` (`isSuccess()`, `getStatusCode()`, `getData()`, `getTotalCount()`, `getErrors()`, `getWarnings()`) |
| `sys.system` | `encrypt`, `encryptWith`, `decrypt`, `decryptWith`, `hash`, `getSecret`, `getSecrets`, `callExternalApi`, `callExternalApis`, `getToken`, `getTokens`, `executeQuery`, `getTableMeta`, `getTableMetas`, `createIndexes`, `dropIndexes`, `dropIndexesOfMany`, `getIndexes`, `getIndexesOfMany`, `emitEvent`, `emitEventWS`, `isValidDataForTable(s)`, `isValidDataForCustomAPI(s)`, `multiTenantInstance(s)Updated`, `isValidConnectionString(s)`, `getLock`, `releaseLock` |
| `sys.cache` | `getKey`, `getKeys`, `setKey`, `setKeys`, `removeKey`, `removeKeys`, `resetCacheDB`, `resetCacheDBs`, `resetCacheCustomApis`, `resetCacheCustomApisOfTenant`, `resetCacheSystemApis`, `resetCacheSystemApisOfTenant` |
| `sys.call("db.getAll", …)` | any API by its name in JavaScript, with its arguments as they are |
| `JsonObject` | `of`, `from`, `getString`, `getLong`, `getInt`, `getDouble`, `getBoolean`, `getObject`, `objectAt`, `getList`, `getObjects`, `getStrings`, `has`, `with`, `at` |
| `logger` | `log`, `debug`, `info`, `warn`, `error` |
| `shared` | a `ConcurrentHashMap<String, Object>` of the runner |
| `Json` | `stringify`, `pretty`, `parse`, and the paths of any value : `get`, `text`, `number`, `integer`, `truth`, `object`, `array` |
| errors | `throw new AmErrors(message, code)` or `new AmErrors(list)`; `.errors` (a `List<ResponseError>`), `.code` |

## Debugging

- The debug runner of Java speaks JDWP with the server : the **Debug** tab of the API testing page shows where the code stopped.
- Every piece of code of Java is a class `Api` : the debugger tells the classes of each file apart.
- The debugger evaluates variables, fields, items of arrays and operators : it calls no method.
- Learn more [Debugging](https://docs.apimaker.dev/v4/docs/features/debugging.html).

## Related

- [Kotlin](https://docs.apimaker.dev/v4/docs/languages/kotlin.html) · [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.html) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html)
