MicroProfile 4.0 Changes

MicroProfile 4.0 was released on December 2020 and announced here.

Let’s see the most relevant changes between 3.0 and 4.0 releases!

Changes that affect all modules

MicroProfile Config 2.0

  • Added bulk extraction of properties into POJO using @ConfigProperties

Now, we can inject a group of properties with the same prefix into a POJO.

For example, having these properties:

server.host = localhost
server.port=9080
server.endpoint=query
server.old.location=London

Then, we can map these properties onto a POJO class doing:

@ConfigProperties(prefix="server")
@ApplicationScoped
public class Details {
    String host;
    int port;
    String endpoint;
    @ConfigProperty(name="old.location")
    String location;
}
  • Property Expression Enhancements

The logic to resolve properties has been enhanced. For example, we can now do:

server.url=http://${server.host}/endpoint
server.host=example.org

And the property server.url will be http://example.org/endpoint.

Moreover, we can achieve the following syntact for property expressions:

server.url=http://${server.host:default.org}/endpoint # default properties
server.url=http://${host:${default-host}}/endpoint # nested properties
server.url=http://${server.host}/${path} # composed properties
server.url=\\${server.host} # no substitution: we would get "${server.host}"
Note that this can be a breaking change, so if you need to disable it by default, you need to set mp.config.property.expressions.enabled to false.

Finally, we can inject directly ConfigValue in our Java classes:

@Dependent
public class MyBean {
    @Inject
    @ConfigProperty(name = "server.host")
    private ConfigValue configValue;

    // configValue.getName() -> server.host
    // configValue.getValue() -> example.org
    // configValue.getSourceName() -> where the value is coming from
    // configValue.getSourceOrdinal() -> the ordinal of the injected value (depends on the source)
    // configValue.getRawValue() -> No substitution: ${server.host}
}
  • Added configuration profiles (ex: dev, testing, live)

We can now control the profile we’re using in MicroProfile using the property mp.config.profile. And then we can make use of this properties via the properties:

%dev.vehicle.name=car
%live.vehicle.name=train
%testing.vehicle.name=bike
vehicle.name=lorry

Or directly via the properties files:

META-INF\microprofile-config.properties
META-INF\microprofile-config-dev.properties
META-INF\microprofile-config-prod.properties
META-INF\microprofile-config-testing.properties
  • Changes in the ConfigSource interface

By adding your own implementation of the ConfigSource interface, you can provide a new way to inject properties into your application.

As an example, we have implemented a in memory config source where we’ll keep properties in a in memory map:

public class CustomConfigSource implements ConfigSource {

    private static final int ORDINAL = 999;

    Map<String, String> customProperties = new HashMap<>();

    @Override
    public Map<String, String> getProperties() {
        return customProperties;
    }

    @Override
    public int getOrdinal() {
        return ORDINAL;
    }

    @Override
    public String getValue(String propertyName) {
        return customProperties.get(propertyName);
    }

    @Override
    public String getName() {
        return "Custom Config Source";
    }
}

The above implementation will not compile when using MicroProfile 4.x since now the method getProperties() is optional and the method getPropertyNames has to be provided:

public class CustomConfigSource implements ConfigSource {

    private static final int ORDINAL = 999;

    Map<String, String> customProperties = new HashMap<>();

    @Override
    public Set<String> getPropertyNames() {
        return customProperties.keySet();
    }

    @Override
    public int getOrdinal() {
        return ORDINAL;
    }

    @Override
    public String getValue(String propertyName) {
        return customProperties.get(propertyName);
    }

    @Override
    public String getName() {
        return "Custom Config Source";
    }
}
  • Empty values are no longer valid

Having this property:

server.url=

It will throw a NoSuchElementException exception now.

MicroProfile Health 3.0

  • Default Readiness Statuses (UP/DOWN) for empty check responses

Prior to MicroProfile 4.0, the status for an empty response was always DOWN. Now, we can control the default status for empty responses by setting:

mp.health.default.readiness.empty.response=UP
  • The annotation @Health has been removed

If you were implementation a custom health check such as:

@Health
@ApplicationScoped
public class GreetingHealthCheck implements HealthCheck {

    @Override
    public HealthCheckResponse call() {
        return HealthCheckResponse.builder().name("greeting").up().build();
    }
}

You would need to change the annotation @Health to either @Readiness or @Liveness depends on what you’re doing in the health check.

  • HealthCheckResponse.state is renamed to status

Before:

@Produces
@Liveness
HealthCheck check() {
    return () -> HealthCheckResponse.named("heap-memory").state(getMemUsage() < 0.9).build();
}

After:

@Produces
@Liveness
HealthCheck check() {
    return () -> HealthCheckResponse.named("heap-memory").status(getMemUsage() < 0.9).build();
}

MicroProfile JWT Auth 1.2

  • New method has been added to get claims from JsonWebToken
JsonWebToken token = ...
long issuedAtTime = token.getClaim(Claims.iat);
Set<String> groups = token.getClaim(Claims.groups);
// ...
  • Support for JWT token cookies

Before, MicroProfile only supported the header Authentication to set the Bearer JWT token. Now, this token can be retrieved from the cookies by setting:

mp.jwt.token.header=Cookie # where to find the token. Either `Authorization` or `Cookie`
mp.jwt.token.cookie=mycookiename # the cookie name where to get the JWT token. Default is `Bearer`.

MicroProfile Metrics 3.0

  • Scopes: Base, Application, Vendor

The following three sets of sub-resource (scopes) are exposed.

  • base: metrics that all MicroProfile vendors have to provide and exposed under /metrics/base
  • vendor: vendor specific metrics (optional) and exposed under /metrics/application
  • application: application-specific metrics (optional) and exposed under /metrics/vendor

  • Timer now exposes total elapsed time duration as a metric value

Having this bean with the @Timer annotation:

public class TimedMethodBean {

    @Timed(name = "timedMethod")
    public void timedMethod() {
        // do something
    }
}

Now, we can get the elapsed time this way:

public class TimedMethodBeanLookupTest {

    private final static String TIMER_NAME = MetricRegistry.name(TimedMethodBean.class, "timedMethod");

    private static MetricID timerMID;

    @Inject
    private MetricRegistry registry;

    @Inject
    private TimedMethodBean bean;

    @Test
    @InSequence(2)
    public void getElapsedTime() {
        Timer timer = registry.getTimers().get(timerMID);

        // Call the timed method and assert it's been timed
        bean.timedMethod();

        // Make sure that the timer has been called
        assertNotNull(timer.getElapsedTime());
    }

  • New REST metric for unmapped endpoints

This is a super useful enhancement to retrieve the number of unmapped REST endpoints has been invoked. The new metric name is called base_REST_request_unmappedException_total and counts the occurrences of unmapped exceptions for each REST endpoint.

  • CDI producers annotated with @Metric no longer trigger metric registration

Before, when having this producer:

@Produces
@Metric(name = "hitPercentage")
@ApplicationScoped
protected Gauge<Double> createHitPercentage() {
    return // ...
}

The hitPercentage metric was automatically registered. Now, we need to explicitly register it using the MetricRegistry:

@Inject
MeterRegistry registry;

@Inject
Gauge<Double> hitPercentageGauge;

// ...
registry.register("hitPercentage", hitPercentageGauge);
  • MetricRegistry changed from abstract class to interface
  • Changed Timer.update(long duration, java.util.concurrent.TimeUnit) to Timer.update(java.time.Duration duration)
  • Changes in the MetadataBuilder API
Old Method New Method Accepts Null Values
withOptionalDisplayName withDisplayName Yes
withOptionalDescription withDescription Yes
withOptionalType withType Yes
withOptionalUnit withUnit Yes
  • Changes in the Metadata API

Changed existing getDescription() and getUnit() methods to return String (before they returned Optional). Added new methods `description()` and `unit()` to return Optional.

MicroProfile Rest Client 2.0

  • Added support for Server Sent Events

Server Sent Events, part of the HTML 5 spec, enable a server to push data to a client asynchronously via events, over HTTP and is now supported in MicroProfile Rest Client!

The MicroProfile Rest Client specification uses the Reactive Streams APIs to consume events:

@RegisterRestClient
public interface EventsClient {
    @GET
    @Path("/events/sse")
    @Produces(MediaType.SERVER_SENT_EVENTS)
    Publisher<String> getEvents();
}
  • Added support for configuring HTTP proxy servers

If a REST endpoint needs to be accessed using a proxy server, we can now configure MicroProfile Rest Client to use it. Via properties:

com.sgitario.EventsClient/mp-rest/proxyAddress=myproxy.com:2000

Or programmatically:

EventsClient client = RestClientBuilder.newBuilder()
                                    .proxyAddress("myproxy.com", 2000)
                                    //...
                                    .build(EventsClient.class);
  • Added support for automatically following redirect requests

If a REST endpoint has been moved, we’ll get a 300 HTTP code to indicate the new location. Now, we can configure the MicroProfile Rest Client to automatically redirect onto the new location by configurating the property:

com.sgitario.EventsClient/mp-rest/followRedirects=true

Or by doing it in Java:

EventsClient client = RestClientBuilder.newBuilder()
                                    .followRedirects(true)
                                    //...
                                    .build(EventsClient.class);

MicroProfile Fault Tolerance 3.0

  • Metric names and scopes changed

There are two breaking changes in here: (1) the scope of the MicroProfile Fault Tolarence metrics have been moved from application to base; and (2) the method names are now a method tag.

Old metric:

application:ft.<name>.timeout.callsTimedOut.total

New metric:

base:ft.timeout.calls.total{method="<name>", timedOut="true"}
  • Lifecycle of circuit breakers and bulkheads is now Singleton

Therefore, even when doing something like:

@ApplicationScoped // or @RequestScoped
public class MyService {

    @CircuitBreaker(requestVolumeThreshold=2, failureRatio=0.5)
    public void doSomething() {
        // ...
    }
}

The MyService bean will use the specified scope (Application or Request or other), but the circuit breaker will be a Singleton.

MicroProfile OpenAPI 2.0

  • Added @SchemaProperty annotation

Previously, we had to use the same @Schema annotation to define properties, for example:

@Schema(name = "Car", description = "Car entity")
public class Car {
    @Schema(description = "The vendor")
    String vendor;
    Engine engine;
}

@Schema(name = "Engine", description = "Engine entity")
public class Engine {
    String type;
}

It would generate the next OpenAPI definition:

openapi: 3.0.3
info:
  title: Generated API
  version: "1.0"
paths:
components:
  schemas:
    Car:
      description: Car entity
      type: object
      properties:
        engine:
          $ref: '#/components/schemas/Engine'
        vendor:
          description: The vendor
          type: string
    Engine:
      description: Engine entity
      type: object
      properties:
        type:
          type: string

Now, we can do this using the new @SchemaProperty annotation in one go:

@Schema(name = "Car", description = "Car entity", properties = {
        @SchemaProperty(name = "vendor", description = "The vendor"),
        @SchemaProperty(name = "engine", implementation = Engine.class)
})
public class Car {
    String vendor;
    Engine engine;
}

This can be useful when having some complex class hierarchy.

  • Added @APIResponseSchema annotation

These annotations ease the way to add descriptions to requests and responses.

Previously, to define a list of objects in an operation, we had to do:

@GET
@Operation(summary = "Retrieve all Cars")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = Car.class)))
public Response getCars() {
    // ...
}

Now:

@GET
@Operation(summary = "Retrieve all Cars")
@APIResponseSchema(responseCode = "200", value = Car[].class) // or @APIResponseSchema(Car[].class)
public Response getCars() {
    // ...
}
  • Added @RequestBodySchema annotation

For requests body, we can now define the schema this way:

@POST
@Operation(summary = "Add Car")
@Produces(MediaType.TEXT_PLAIN)
public void updateCar(@RequestBodySchema(Car.class) Car car) {
    // ...
}
  • Added mp.openapi.schema MicroProfile Config property prefix

What if we want to add some description to some sources that we can’t modify like java.time.Instant? We can now do this using the mp.openapi.schema properties:

mp.openapi.schema.java.time.Instant = { \
        "name": "EpochSeconds", \
        "type": "number", \
        "format": "int64", \
        "title": "Epoch Seconds", \
        "description": "Number of seconds from the epoch of 1970-01-01T00:00:00Z" \
    }
  • A lot of removals and updates in the OpenAPI API

If you would like to see the full list of breaking changes, go to here.

MicroProfile OpenTracing 2.0

API deletions:

  • ScopeManager.active(): no alternative, the reference Scope has to be kept explicitly since the scope was created.
  • ScopeManager.activate(Span, boolean): no alternative auto-finishing has been removed.
  • Scope.span(): use ScopeManager.activeSpan() or hold the reference to Span explicitly since the span was started.
  • SpanBuilder.startActive(): use Tracer.activateSpan(Span) instead.
  • Tracer.startManual(): use Tracer.start() instead.
  • AutoFinishScopeManager: no alternative, auto-finishing has been removed.
[ Java ]