1.17.0 release notes

6th July 2022

🌟 New features

  • You can now easily send and receive RESTful APIs using RestClient. #4263

    • Java
      RestClient restClient = RestClient.of("...");
      CompletableFuture<ResponseEntity<Customer>> response =
          restClient.get("/api/v1/customers/{customerId}")
                    .pathParam("customerId", "0000001")
                    .execute(Customer.class);
    • Kotlin
      val restClient: RestClient = RestClient.of("...");
      val response: ResponseEntity<Customer> =
         restClient
           .get("/api/v1/customers/{customerId}")
           .pathParam("customerId", "0000001")
           .execute<Customer>()  // a suspend function
    • Scala
      val restClient: ScalaRestClient = ScalaRestClient("...")
      val response: Future[ResponseEntity[Result]] =
        restClient.post("/api/v1/customers")
                  .contentJson(new Customer(...))
                  .execute[Result]()
  • You can now configure a timeout for an Endpoint selection of an DynamicEndpointGroup. #4246

    DnsAddressEndpointGroup delegate = DnsAddressEndpointGroup.of(...);
    HealthCheckedEndpointGroup healthGroup =
      HealthCheckedEndpointGroup
        .builder(delegate, "/health")
        .selectionTimeout(Duration.ofSeconds(10)) // 👈👈👈
        .build();
  • You can now inject dependencies in an annotation using DependencyInjector. #4006 #4202

    // Inject authClient that is needed to create the AuthDecorator.
    WebClient authClient = ...
    DependencyInjector injector =
        DependencyInjector.ofSingletons(new AuthDecorator(authClient));
    serverBuilder.dependencyInjector(dependencyInjector, true);
    
    // An annotated service that uses AuthDecorator.
    @Get("/foo")
    @Decorator(AuthDecorator.class)
    public FooResponse foo(FooRequest req) {
        // Authrorized request.
        ...
    }
    
    // authClient is injected.
    class AuthDecorator implements DecoratingHttpServiceFunction {
        AuthDecorator(WebClient authClient) { ... }
    
        @Override
        public HttpResponse serve(HttpService delegate,
                                  ServiceRequestContext ctx,
                                  HttpRequest req)
                throws Exception {
            // Authorize the request.
            ...
        }
    }
    • Set armeria.enable-auto-injection to true to apply SpringDependencyInjector automatically when using Spring Boot integration.
      // in application.yml
      armeria:
        ports:
          ...
        enable-auto-injection: true  // 👈👈👈
  • You can now customize the length of string fields or container fields of Thrift messages using THttpServiceBuilder or ThriftClientBuilder. #4024 #4226

    THttpService.builder()
                .addService(new MyThriftService())
                .maxRequestStringLength(MAX_STRING_LENGTH)
                .maxRequestContainerLength(MAX_CONTAINER_LENGTH)
                .build();
    ThriftClients.builder("https://my.server.com")
                 .path("/thrift")
                 .maxResponseStringLength(MAX_STRING_LENGTH)
                 .maxResponseContainerLength(MAX_CONTAINER_LENGTH)
                 .build(HelloService.AsyncIface.class);
  • You can now set attributes to an Endpoint using Attributes and AttributesBuilder. #4241

    AttributeKey<String> region = AttributeKey.valueOf(MyAttrs.class, "region");
    Attributes attrs =
      Attributes
        .builder()
        .set(region, "us-west-1")
        .build();
    
    Endpoint endpointA = ...;
    Endpoint endpointB = endpointA.withAttrs(attrs);
    assert endpointB.attr(region).equals("us-west-1");
    assert !endpointA.equals(endpointB)
  • You can now use MultipartFile to get the actual filename of an uploaded file through multipart/form-data. #4262

    @Consumes(MediaTypeNames.MULTIPART_FORM_DATA)
    @Post("/upload")
    public HttpResponse upload(MultipartFile multipartFile) {
      // The name parameter of the "content-disposition" header
      String name = multipartFile.name();
      // The filename parameter of the "content-disposition" header
      String filename = multipartFile.filename();
      // The file that stores the multipart content.
      File file = multipartFile.file();
      ...
    }
  • You can now read a range of bytes using ByteStreamMessage. #4058

    Path path = ...;
    // Fluently build a `StreamMessage` for a `Path`.
    ByteStreamMessage pathContent =
        StreamMessage.builder(path)
                     .bufferSize(1024)
                     .alloc(alloc)
                     .executor(executor)
                     .build();
    // Slice a range of a file data
    ByteStreamMessage partialContent = pathContent.range(100, 200);
  • You can now map a specific exception to a LogLevel in LoggingService and LoggingClient. #3400 #4090

    LoggingService.builder()
                  .responseLogLevel(IllegalStateException.class,
                                    LogLevel.WARN)
                  .responseLogLevel(IllegalArgumentException.class,
                                    LogLevel.ERROR)
                  ...
  • You can now map a specific exception to an HttpResponse. #4279 #4283

    HttpResponse recovered =
        response.recover(IllegalStateException.class,
                         cause -> HttpResponse.of("Fallback"));
  • You can now enable automatic compression for a gRPC response. #4258 #4266

    GrpcService.builder()
               .addService(new MyGrpcService())
               .autoCompression(true)
               .build();
    • This only works when the client sends a grpc-accept-encoding header and the CompressorRegistry has the compressor.
  • You can now add a hook to Server and ClientFactory that is called when the JVM shuts down. #4015 #4043

    server.closeOnJvmShutdown(() -> {
      System.err.println("Server is stopping soon.");
    }).thenRun(() -> {
      System.err.println("Server has stopped.");
    });
  • You can now customize internal Netty ChannelPipeline using ClientFactoryOptions.CHANNEL_PIPELINE_CUSTOMIZER. #3907 #4260

    • This is an advanced feature and not recommended to use if you are not familiar with Armeria and Netty internals.

Kotlin

  • You can now use the Kotlin nullable type (?) to indicate a nullable parameter in an annotated service. #4144 #4225
    serverBuilder.apply {
      annotatedService(object {
        // Both `/foo` and `/foo?a=bar` are allowed.
        @Get("/foo")
        fun foo(@Param a: String?) = // 👈👈👈
          HttpResponse.of("a: $a")
      })
    }

📈 Improvements

🛠️ Bug fixes

🏚️ Deprecations

☢️ Breaking changes

⛓ Dependencies

  • Brave 5.13.8 → 5.13.9
  • Bucket4J 7.4.0 → 7.5.0
  • Dropwizard metrics 4.2.9 → 4.2.10
  • GraphQL 1.8.0 → 1.8.2
  • gRPC-Java 1.45.1 → 1.47.0
  • gRPC-Kotlin 1.2.1 → 1.3.0
  • Jackson 2.13.2.1 → 2.13.3
  • Kotlin 1.6.20 → 1.7.0
  • Kotlinx 1.6.1 → 1.6.3
  • Micrometer 1.8.5 → 1.9.1
  • Netty 4.1.76.Final → 4.1.78.Final
  • Netty incubator 0.0.13.Final → 0.0.14.Final
  • Project Reactor 3.4.17 → 3.4.19
  • Prometheus Simpleclient 0.15.0 → 0.16.0
  • Reactive Streams 1.0.3 → 1.0.4
  • RxJava 3.1.4 → 3.1.5
  • ScalaPB 0.11.10 → 0.11.11
  • Spring 5.3.19 → 5.3.21
  • Spring Boot 2.6.6 → 2.7.1

🙇 Thank you