1.15.0 release notes
24th March 2022
🌸 Highlights
- gRPC health check protocol
- Multipart file uploads in annotated services
- Preliminary GraphQL support in
DocService
- Refactored DNS resolver cache with higher hit ratio and reduced traffic
- New metrics that help you get alerts before your TLS certificates are expired
🌟 New features
Server
now registers two gauges about TLS certificate expiry, so you can get alerts before the certificates are expired. #4075 #4082armeria_server_certificate_validity
1
if certificate is in validity period.0
otherwise.
armeria_server_certificate_validity_days
- The number of days before certificate expires, or
-1
if expired.
- The number of days before certificate expires, or
You can now specify a
File
orPath
parameter in your annotated service method to handle multipart file uploads. #3578 #4061Server .builder() .annotatedService(new Object() { @Post("/upload") @Consumes("multipart/form-data") public String upload(@Param File file) { return "Successfully uploaded a file to " + file; } }) .build();
You can now use the
||
(logical OR) operator in parameter and header predicate expressions. #4116 #4138// "Hi!" for /greet?casual or /greet?casual=true // "Hello!" for /greet or /greet?casual=false Server .builder() .route() .get("/greet") .matchesParam("casual || casual=true") // 👈👈👈 .build((ctx, req) -> HttpResponse.of("Hi!")) .route() .get("/greet") .matchesParam("!casual || casual!=true") // 👈👈👈 .build(ctx, req) -> HttpResponse.of("Hello!"))
You can now enable gRPC health check protocol with
GrpcHealthCheckService
#3146 #3963Server .builder() .service( GrpcService .builder() .addService(new MyGrpcService()) .enableHealthCheckService(true) // 👈👈👈 // or customize: // .addService(GrpcHealthCheckService.of(...)) .build() ) .build()
DocService
now supportsGraphqlService
with CodeMirror auto-completion. #3706 #4023You can now configure the
EndpointGroup
implementations based onDynamicEndpointGroup
to allow or disallow empty endpoints. This can be useful when you want to avoid the situation where the endpoint group becomes empty due to misconfiguration. #3952 #3958EndpointGroup endpointGroup = DnsAddressEndpointGroup .builder("example.com") .allowEmptyEndpoints(false) // 👈👈👈 .build();
You can now configure the timeout of DNS sub-queries with
queryTimeoutMillisForEachAttempt()
. Previously, you were only able to specify the timeout for the entire DNS resolution process, which can consist of more than one DNS query. #2940 #4133DnsAddressEndpointGroup .builder("armeria.dev") .queryTimeoutMillisForEachAttempt(1000) // 👈👈👈 .queryTimeoutMillis(5000) .build();
Armeria now has the global shared DNS cache by default for higher hit ratio and less DNS traffic. You can also build and share a custom DNS cache across multiple resolvers. #2940 #4133
DnsCache dnsCache = DnsCache .builder() .ttl(60, 3600) .negativeTtl(600) .build(); EndpointGroup endpointGroup = DnsAddressEndpointGroup .builder("armeria.dev") .dnsCache(dnsCache) // 👈👈👈 .build(); ClientFactory factory = ClientFactory .builder() .domainNameResolverCustomizer(builder -> { builder.dnsCache(dnsCache); // 👈👈👈 }) .build(); WebClient client = WebClient .builder(SessionProtocol.HTTPS, endpointGroup) // 👈👈👈 .factory(factory) // 👈👈👈 .build();
Failed search domain queries are now cached according to the negative TTL configuration. #2940 #4133
You can now specify a
SuccessFunction
when constructing a client or server, and let all decorators use it for determining whether a request was successful or not. #4101Server .builder() .successFunction((ctx, log) -> { // 👈👈👈 // Treat only '200 OK' and '204 No Content' as success. switch (log.responseStatus().code()) { case 200: case 204: return true; default: return false; } }) .decorator(LoggingService.newDecorator()) .decorator(MetricCollectingService.newDecorator()) ... WebClient .builder() .successFunction(...) // 👈👈👈 .decorator(LoggingClient.newDecorator()) .decorator(MetricCollectingClient.newDecorator()) .build();
You can now specify different sampling ratio (or
Sampler
) for successful and failed requests when usingLoggingClient
andLoggingService
#3666 #4101LoggingService .builder() // Log at 10% sampling ratio if successful. .successSamplingRate(0.1) // Log all failure. .failureSamplingRate(1.0) .newDecorator()
You can now customize how
LoggingClient
andLoggingService
determines log level more flexibly. #3972LoggingService .builder() // Log at INFO if under /important. DEBUG otherwise. .requestLogLevelMapper(log -> { if (log.requestHeaders().path().startsWith("/important/")) { return LogLevel.INFO; } else { return LogLevel.DEBUG; } }) // Log at INFO if 200 OK. .responseLogLevel(HttpStatus.OK, LogLevel.INFO) // Log at WARN if 5xx. .responseLogLevel(HttpStatusClass.SERVER_ERROR, LogLevel.WARN) .newDecorator()
You can now specify a customizer that customizes a
ClientRequestContext
when building a client. #4075 #4082WebClient client = WebClient .builder() .contextCustomizer(ctx -> { // 👈👈👈 String userId = MyUserContext.current().getId(); ctx.setAttr(USER_ID, userId); }) .build();
For instance, you could manually propagate a Brave thread-local
TraceContext
using it:Tracing reactorTracing = ...; Tracing requestContextTracing = Tracing .newBuilder() .currentTraceContext(RequestContextCurrentTraceContext.ofDefault()) .build(); WebClient .builder() .contextCustomizer(TraceContextPropagation.inject(() -> { // Propagate Reactor's TraceContext to Armeria's TraceContext. return reactorTracing.currentTraceContext().get(); }) .decorator(BraveClient.newDecorator(requestContextTracing)) .build();
You can now write the content of
StreamMessage
into a file usingStreamMessage.writeTo()
#4048 #4130SplitHttpResponse res = WebClient .of() .get("https://example.com/large_file") .split(); StreamMessage<HttpData> body = res.body(); body.writeTo( // 👈👈👈 Function.identity(), Path.of("/tmp", "large_file") );
You can now convert a
StreamMessage
into anInputStream
usingStreamMessage.toInputStream()
#4059StreamMessage<HttpData> body = ...; BufferedReader in = new BufferedReader( new InputStreamReader( body.toInputStream(Function.identity()), // 👈👈👈 "UTF-8" ) ); for (;;) { String line = in.readLine(); if (line == null) break; System.out.println(line); }
You can now use
StreamMessage.mapParallel()
to modify a stream using an async operation with a configurable concurrency limit. #4031You can now decode a
StreamMessage
into another usingStreamMessage.decode()
which was previously possible only forHttpMessage
. #4147 #4148 #4152You can now give Armeria a performance optimization hint by specifying if your service is unary, request-streaming, response-streaming or bidi-streaming by implementing
HttpService.exchangeType()
#3956class MyUnaryService implements HttpService { @Override public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) { return HttpResponse.from( req .aggregate() // Always aggregate .thenApply(aggregatedReq -> { HttpResponse.of( // Never streaming "You sent %d bytes.", aggregatedReq.content().length() ); }) ); } // Tells Armeria to assume non-streaming requests and responses. @Override public ExchangeType exchangeType() { return ExchangeType.UNARY; // 👈👈👈 } }
📈 Improvements
- Spring Boot
WebServerException
message is now more user-friendly. #4146
🛠️ Bug fixes
- A response with status code
408
doesn't trigger aNullPointerException
anymore, even if its reason phrase is not exactlyRequest Timeout
. #4165 - Failed search domain queries are now cached properly according to the negative TTL configuration. #2940 #4133
GrpcService
now logsRequestOnlyLog.requestContent()
for invalid requests. #4128DocService
doesn't crash the whole Armeria service anymore when.proto
files have comments on service options or extensions. #4123 #4127GraphqlService
doesn't throw aClassCastException
anymore whenquery
oroperationName
are not a string. #4104 #4168FileService
doesn't choose a.br
file for generating decompressed content anymore when Brotli is not available. #4119- You no longer see an
HttpResponseException
orHttpStatusException
from built-in services. #4056 #4117- Thanks to this fix, you can now mutate a redirect response from
FileService
in your decorator usingHttpResponse.mapHeaders()
- Thanks to this fix, you can now mutate a redirect response from
RampingUpEndpointWeightSelector
doesn't raise aNoSuchElementException
anymore when endpoints are replaced completely. #3776 #4102
🏚️ Deprecations
samplingRate
property of@LoggingDecorator
has been deprecated in favor ofsuccessSamplingRate
andfailureSamplingRate
.- The
successFunction()
builder methods that require aBiPredicate
has been deprecated in favor ofSuccessFunction
that's specified when constructing a client or server. dnsServerAddressStreamProvider()
has been deprecated in favor ofserverAddressStreamProvider()
in the DNS-related builders.disableDnsQueryMetrics()
has been deprecated in favor ofenableDnsQueryMetrics()
in the DNS-related builders.
☢️ Breaking changes
- The experimental
HttpDecoder
API has been replaced withStreamDecoder
#4147 #4152
⛓ Dependencies
- Bucket4j 7.0.0 → 7.3.0
- Curator 5.2.0 → 5.2.1
- Dropwizard Metrics 4.2.7 → 4.2.9
- gRPC-Java 1.43.2 → 1.45.0
- Jackson 2.13.1 → 2.13.2
- java-jwt 3.18.3 → 3.19.0
- Jetty 9.4.44 → 9.4.45
- Graphql-Java 17.3 → 18.0
- Logback 1.2.10 → 1.2.11
- Micrometer 1.8.2 → 1.8.4
- Netty 4.1.73 → 4.1.75
- io_uring 0.0.11 → 0.0.13
- Prometheus 0.14.1 → 0.15.0
- Reactor 3.4.14 → 3.4.16
- Reactor Kotlin 1.1.5 → 1.1.6
- RxJava 3.1.3 → 3.1.4
- Sangria 2.1.6 → 3.0.0
- ScalaPB 0.11.8 → 0.11.10
- scala-collection-compat 2.6.0 → 2.7.0
- SLF4J 1.7.34 → 1.7.36
- Spring 5.3.15 → 5.3.17
- Spring Boot 2.6.3 → 2.6.5