Skip to content

Latest commit

 

History

History
441 lines (359 loc) · 13.8 KB

File metadata and controls

441 lines (359 loc) · 13.8 KB

Discord Build Maven Central javadoc javadoc License

HTTP server and client libraries via code generation.

A light (~80kb) wrapper to the JDK 11+ Java Http Client. Additionally, you can create Feign-style interfaces and have implementations generated via annotation processing.

  • Fluid API for building URLs and payload
  • JSON marshaling using Avaje Jsonb/Jackson/Gson
  • Light Feign-style interfaces via annotation processing.
  • Request/Response Interception
  • Authorization via Basic Auth or OAuth Bearer Tokens
  • Async and sync API

Use source code generation to adapt annotated REST controllers @Path, @Get, @Post, etc to Javalin, Helidon SE, and similar web routing HTTP servers.

  • Lightweight (65Kb library + generated source code)
  • Reflection free jax-rs style controllers
  • Supports full use of underlying Javalin or Helidon SE constructs as desired
  • Bean Validation of request bodies (validation groups supported as well)

Add dependencies

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-http-api</artifactId>
  <version>${avaje.http.version}</version>
</dependency>

Add the generator module for your desired microframework as an annotation processor.

<!-- Annotation processors -->
<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-http-{jex/javalin/helidon/vertx}-generator</artifactId>
  <version>${avaje.http.version}</version>
  <scope>provided</scope>
</dependency>

JDK 23+

In JDK 23+, annotation processors are disabled by default, you will need to add a flag to re-enable.

<properties>
  <maven.compiler.proc>full</maven.compiler.proc>
</properties>

Define a Controller (These APT processors work with both Java and Kotlin)

package org.example.hello;

import io.avaje.http.api.Controller;
import io.avaje.http.api.Get;
import java.util.List;

@Controller("/widgets")
public class WidgetController {
  private final HelloComponent hello;
  public WidgetController(HelloComponent hello) {
    this.hello = hello;
  }

  @Get("/{id}")
  Widget getById(int id) {
    return new Widget(id, "you got it"+ hello.hello());
  }

  @Get()
  List<Widget> getAll() {
    return List.of(new Widget(1, "Rob"), new Widget(2, "Fi"));
  }

  record Widget(int id, String name){};
}

DI Usage

The annotation processor will generate controller adapters to register routes to Javalin, Helidon, or Vert.x. The natural way to use the generated adapters is to get a DI library to find and wire them. The AP will automatically detect the presence of avaje-inject and generate the class to use avaje-inject's @Component as the DI annotation.

There isn't a hard requirement to use Avaje for dependency injection. In the absence of avaje-inject, the generated class will use @jakarta.inject.Singleton or @javax.inject.Singleton depending on what's on the classpath. Any DI library that can find and wire the generated @Singleton beans can be used. You can even use Dagger2 or Guice to wire the controllers if you so desire.

To force the AP to generate with @javax.inject.Singleton(in the case where you have both jakarta and javax on the classpath), use the compiler arg -AuseJavax=true

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <compilerArgs>
      <arg>-AuseJavax=true</arg>
    </compilerArgs>
  </configuration>
</plugin>

Usage with Javalin

The annotation processor will generate controller classes implementing the AvajeJavalinPlugin interface, which we can register in javalin using:

List<AvajeJavalinPlugin> routes = ...; //retrieve using a DI framework

Javalin.create(cfg -> routes.forEach(cfg::registerPlugin)).start();

Usage with Helidon SE (4.x)

The annotation processor will generate controller classes implementing the Helidon HttpFeature interface, which we can register with the Helidon HttpRouting.

List<HttpFeature> routes = ... //retrieve using a DI framework
final var builder = HttpRouting.builder();

routes.forEach(builder::addFeature);

WebServer.builder()
         .addRouting(builder)
         .build()
         .start();

Usage with Vert.x

Add the Vert.x runtime API dependency:

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-http-api-vertx</artifactId>
  <version>${avaje.http.version}</version>
</dependency>

The annotation processor will generate controller classes implementing VertxRouteSet, which can be registered with io.vertx.ext.web.Router.

List<io.avaje.http.api.vertx.VertxRouteSet> routes = ... //retrieve using a DI framework
Router router = ...;

routes.forEach(route -> route.register(router));

Generated sources

(Javalin) The generated WidgetController$Route.java is:

@Generated("avaje-javalin-generator")
@Singleton
public final class WidgetController$Route extends AvajeJavalinPlugin {

  private final WidgetController controller;

  public WidgetController$Route(WidgetController controller) {
    this.controller = controller;
  }

  @Override
  public void onStart(JavalinState state) {
    routes(state.routes);
  }

  private void routes(RoutesConfig app) {

    app.get("/widgets/{id}", ctx -> {
      ctx.status(200);
      var id = asInt(ctx.pathParam("id"));
      var result = controller.getById(id);
      ctx.json(result);
    });

    app.get("/widgets", ctx -> {
      ctx.status(200);
      var result = controller.getAll();
      ctx.json(result);
    });

  }
}

(Helidon SE) The generated WidgetController$Route.java is:

@Generated("avaje-helidon-generator")
@Component
public final class WidgetController$Route implements HttpFeature {

  private final WidgetController controller;

  public WidgetController$Route(WidgetController controller) {
    this.controller = controller;
  }

  @Override
  public void setup(HttpRouting.Builder routing) {
    routing.get("/widgets/{id}", this::_getById);
    routing.get("/widgets", this::_getAll);
  }

  private void _getById(ServerRequest req, ServerResponse res) throws Exception {
    res.status(OK_200);
    var pathParams = req.path().pathParameters();
    var id = asInt(pathParams.contains("id") ? pathParams.get("id") : null);
    var result = controller.getById(id);
    if (result == null) {
      res.status(NO_CONTENT_204).send();
    } else {
      res.send(result);
    }
  }

  private void _getAll(ServerRequest req, ServerResponse res) throws Exception {
    res.status(OK_200);
    var result = controller.getAll();
    if (result == null) {
      res.status(NO_CONTENT_204).send();
    } else {
      res.send(result);
    }
  }

}

(Vert.x) The generated WidgetController$Route.java is:

@Generated("avaje-vertx-generator")
@Component
public final class WidgetController$Route implements VertxRouteSet {

  private final WidgetController controller;

  public WidgetController$Route(WidgetController controller) {
    this.controller = controller;
  }

  @Override
  public void register(Router router) {

    var routes = router;
    {
      var route = routes.get("/widgets/:id");
      route.handler(ctx -> {
      try {
        ctx.response().setStatusCode(200);
        var id = asInt(ctx.pathParam("id"));
        var result = controller.getById(id);
          if (result == null || ctx.response().ended()) {
            return;
          }
        ctx.response().putHeader("content-type", "application/json");
        ctx.response().end(Json.encode(result));
      } catch (Exception e) {
        ctx.fail(e);
      }
      });
    }
    {
      var route = routes.get("/widgets");
      route.handler(ctx -> {
      try {
        ctx.response().setStatusCode(200);
        var result = controller.getAll();
          if (result == null || ctx.response().ended()) {
            return;
          }
        ctx.response().putHeader("content-type", "application/json");
        ctx.response().end(Json.encode(result));
      } catch (Exception e) {
        ctx.fail(e);
      }
      });
    }
  }
}

Generated sources (Avaje-Jsonb)

If Avaje-Jsonb is detected, http generators with support will use it for faster Json message processing.

(Javalin) The generated WidgetController$Route.java is:

@Generated("avaje-javalin-generator")
@Component
public final class WidgetController$Route extends AvajeJavalinPlugin {

  private final WidgetController controller;
  private final JsonType<List<Widget>> listWidgetJsonType;
  private final JsonType<Widget> widgetJsonType;

  public WidgetController$Route(WidgetController controller, Jsonb jsonb) {
    this.controller = controller;
    this.listWidgetJsonType = jsonb.type(Types.newParameterizedType(List.class, Widget.class));
    this.widgetJsonType = jsonb.type(Widget.class);
  }

  @Override
  public void onStart(JavalinState state) {
    routes(state.routes);
  }

  private void routes(RoutesConfig app) {

    app.get("/widgets/{id}", ctx -> {
      ctx.status(200);
      var id = asInt(ctx.pathParam("id"));
      var result = controller.getById(id);
      widgetJsonType.toJson(result, ctx.contentType("application/json").res().getOutputStream());
    });

    app.get("/widgets", ctx -> {
      ctx.status(200);
      var result = controller.getAll();
      listWidgetJsonType.toJson(result, ctx.contentType("application/json").res().getOutputStream());
    });

  }
}

(Helidon SE) The generated WidgetController$Route.java is:

@Generated("avaje-helidon-generator")
@Component
public final class WidgetController$Route implements HttpFeature {

  private final WidgetController controller;
  private final JsonType<WidgetController.Widget> widgetController$WidgetJsonType;
  private final JsonType<List<WidgetController.Widget>> listWidgetController$WidgetJsonType;

  public WidgetController$Route(WidgetController controller, Jsonb jsonb) {
    this.controller = controller;
    this.widgetController$WidgetJsonType = jsonb.type(WidgetController.Widget.class);
    this.listWidgetController$WidgetJsonType = jsonb.type(Types.newParameterizedType(List.class, WidgetController.Widget.class));
  }

  @Override
  public void setup(HttpRouting.Builder routing) {
    routing.get("/widgets/{id}", this::_getById);
    routing.get("/widgets", this::_getAll);
  }

  private void _getById(ServerRequest req, ServerResponse res) throws Exception {
    res.status(OK_200);
    var pathParams = req.path().pathParameters();
    var id = asInt(pathParams.contains("id") ? pathParams.get("id") : null);
    var result = controller.getById(id);
    if (result == null) {
      res.status(NO_CONTENT_204).send();
    } else {
      res.headers().contentType(MediaTypes.APPLICATION_JSON);
      //jsonb has a special accommodation for helidon to improve performance
      widgetController$WidgetJsonType.toJson(result, JsonOutput.of(res));
    }
  }

  private void _getAll(ServerRequest req, ServerResponse res) throws Exception {
    res.status(OK_200);
    var result = controller.getAll();
    if (result == null) {
      res.status(NO_CONTENT_204).send();
    } else {
      res.headers().contentType(MediaTypes.APPLICATION_JSON);
      listWidgetController$WidgetJsonType.toJson(result, JsonOutput.of(res));
    }
  }

}

(Vert.x) The generated WidgetController$Route.java is:

@Generated("avaje-vertx-generator")
@Component
public final class WidgetController$Route implements VertxRouteSet {

  private final WidgetController controller;
  private final JsonType<List<Widget>> listWidgetJsonType;
  private final JsonType<Widget> widgetJsonType;

  public WidgetController$Route(WidgetController controller, Jsonb jsonb) {
    this.controller = controller;
    this.listWidgetJsonType = jsonb.type(Types.newParameterizedType(List.class, Widget.class));
    this.widgetJsonType = jsonb.type(Widget.class);
  }

  @Override
  public void register(Router router) {

    var routes = router;

    {
      var route = routes.get("/widgets/:id");
      route.handler(ctx -> {
      try {
        ctx.response().setStatusCode(200);
        var id = asInt(ctx.pathParam("id"));
        var result = controller.getById(id);
          if (result == null || ctx.response().ended()) {
            return;
          }
        ctx.response().putHeader("content-type", "application/json");
        ctx.response().end(Buffer.buffer(widgetJsonType.toJsonBytes(result)));
      } catch (Exception e) {
        ctx.fail(e);
      }
      });
    }

    {
      var route = routes.get("/widgets");
      route.handler(ctx -> {
      try {
        ctx.response().setStatusCode(200);
        var result = controller.getAll();
          if (result == null || ctx.response().ended()) {
            return;
          }
        ctx.response().putHeader("content-type", "application/json");
        ctx.response().end(Buffer.buffer(listWidgetJsonType.toJsonBytes(result)));
      } catch (Exception e) {
        ctx.fail(e);
      }
      });
    }
  }
}