Micronaut Functions deployed in AWS Lambda

Learn how to write a function with Micronaut and deploy it to AWS Lambda.

Authors: Sergio del Amo

Micronaut Version: 1.0.0

1 Getting Started

Lets describe the microservices you are going to build through the tutorial.

  • vies-vat-validator - A Function which you will build with Micronaut and deploy to AWS Lambda.

  • invoice - A microservice which exposes an endpoint to check the Value Added Tax (VAT) which should apply to an invoice. It consumes the vies-vat-validator function to ensure the VAT number is valid.

The next diagram illustrates the flow:

microservices and lambda

If you are using Java or Kotlin and IntelliJ IDEA make sure you have enabled annotation processing.

annotationprocessorsintellij

1.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

You will need also an AWS Account.

1.2 Solution

We recommend you to follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.

or

Then, cd into the complete folder which you will find in the root project of the downloaded/cloned project.

2 Writing the Function

Create the function:

mn create-function example.micronaut.vies-vat-validator

2.1 POJOs

Create a POJO to encapsulate the validation request:

{
  "memberStateCode": "es",
  "vatNumber": "B86412491"
}
vies-vat-validator/src/main/java/example/micronaut/VatValidationRequest.java
package example.micronaut;

import java.io.Serializable;

public class VatValidationRequest implements Serializable {
    private String memberStateCode;
    private String vatNumber;

    public VatValidationRequest() {
    }

    public VatValidationRequest(String memberStateCode, String vatNumber) {
        this.memberStateCode = memberStateCode;
        this.vatNumber = vatNumber;
    }

    public String getMemberStateCode() {
        return memberStateCode;
    }

    public void setMemberStateCode(String memberStateCode) {
        this.memberStateCode = memberStateCode;
    }

    public String getVatNumber() {
        return vatNumber;
    }

    public void setVatNumber(String vatNumber) {
        this.vatNumber = vatNumber;
    }
}

And another POJO to encapsulate the expected response. Note: we add a Boolean valid property.

{
  "memberStateCode": "es",
  "vatNumber": "B86412491"
  "valid": true
}
vies-vat-validator/src/main/java/example/micronaut/VatValidation.java
package example.micronaut;

import java.io.Serializable;
import java.util.Objects;

public class VatValidation extends VatValidationRequest implements Serializable {
    private Boolean valid;

    public VatValidation() {

    }

    public VatValidation(String memberStateCode, String vatNumber, Boolean valid) {
        super(memberStateCode, vatNumber);
        this.valid = valid;
    }

    public Boolean getValid() {
        return valid;
    }

    public void setValid(Boolean valid) {
        this.valid = valid;
    }

    public Boolean isValid() {
        return valid;
    }
}

2.2 SOAP VAT Validation

VIES (VAT Information Exchange System) is an electronic mean of validating VAT-identification numbers of economic operators registered in the European Union for cross border transactions on goods or services.

For example, if you create an e-commerce Web application in the European union you would need to check the validity of the purchaser’s VAT Number in order to create a proper invoice.

To automate the validation checks, the EU does not offer a REST endpoint but a SOAP service. Its WSDL file can be obtained here.

SOAP (originally Simple Object Access Protocol) is a protocol specification for exchanging structured information in the implementation of web services in computer networks. Its purpose is to induce extensibility, neutrality and independence

Create VatService to do a SOAP request to validate the VAT Number.

To consume the SOAP service we could have use a SOAP library. However, the use case is so simple we are going to use the Micronaut HTTP Client and build the SOAP envelope manually.

Add http-client dependency to build.gradle

vies-vat-validator/build.gradle
    compile "io.micronaut:micronaut-http-client"

Create a VatService which consumes the SOAP Service:

vies-vat-validator/src/main/java/example/micronaut/VatService.java
package example.micronaut;

import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.RxHttpClient;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.Flowable;
import io.reactivex.Single;

import javax.inject.Singleton;

@Singleton (1)
public class VatService {
    private static final String SERVER = "http://ec.europa.eu";
    private static final String PATH = "/taxation_customs/vies/services/checkVatService";
    private static final String VALID_XML_OPEN_TAG = "<valid>";
    private static final String VALID_XML_CLOSE_TAG = "</valid>";

    protected final RxHttpClient client;

    public VatService(@Client("http://ec.europa.eu") RxHttpClient client) { (2)
        this.client = client;
    }

    public Single<Boolean> validateVat(String memberStateCode, String vatNumber) {
        String soapEnvelope = soapEnvelope(memberStateCode, vatNumber);
        HttpRequest request = HttpRequest.POST(SERVER+PATH, soapEnvelope)  (3)
                .contentType("application/soap+xml");

        Flowable<String> response = client.retrieve(request, String.class);
        return response.firstOrError().map(this::parseResponseToBoolean);
    }

    private Boolean parseResponseToBoolean(String response) {
        if (!response.contains(VALID_XML_OPEN_TAG) || !response.contains(VALID_XML_CLOSE_TAG)) {
            return false;
        }
        int beginIndex = response.indexOf(VALID_XML_OPEN_TAG) + VALID_XML_OPEN_TAG.length();
        String validResponse = response.substring(beginIndex, response.indexOf(VALID_XML_CLOSE_TAG));
        return Boolean.valueOf(validResponse);
    }

    private String soapEnvelope(String memberStateCode, String vatNumber) {
        StringBuilder sb = new StringBuilder();
        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        sb.append("<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">");
        sb.append("<soapenv:Header/>");
        sb.append("<soapenv:Body xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">");
        sb.append("<urn:checkVat xmlns:urn=\"urn:ec.europa.eu:taxud:vies:services:checkVat:types\">");
        sb.append("<urn:countryCode>");
        sb.append(memberStateCode);
        sb.append("</urn:countryCode>");
        sb.append("<urn:vatNumber>");
        sb.append(vatNumber);
        sb.append("</urn:vatNumber>");
        sb.append("</urn:checkVat>");
        sb.append("</soapenv:Body>");
        sb.append("</soapenv:Envelope>");
        return sb.toString();
    }
}
1 Use javax.inject.Singleton to designate a class a a singleton
2 Constructor Injection of Micronaut RxClient
3 Define a POST Request.

2.3 Function

Edit ViesVatValidatorFunction

vies-vat-validator/src/main/java/example/micronaut/ViesVatValidatorFunction.java
package example.micronaut;

import io.micronaut.function.FunctionBean;
import io.reactivex.Flowable;
import io.reactivex.Single;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ExecutionException;
import java.util.function.Function;

@FunctionBean("vies-vat-validator") (1)
public class ViesVatValidatorFunction
        implements Function<VatValidationRequest, VatValidation> { (2)
    private static final Logger LOG = LoggerFactory.getLogger(ViesVatValidatorFunction.class); (3)

    private final VatService vatService;

    public ViesVatValidatorFunction(VatService vatService) { (4)
        this.vatService = vatService;
    }

    @Override
    public VatValidation apply(VatValidationRequest request) {
        final String memberStateCode = request.getMemberStateCode();
        final String vatNumber = request.getVatNumber();
        if (LOG.isDebugEnabled()) {
            LOG.debug("validate country: {} vat number: {}", memberStateCode, vatNumber);
        }
        return vatService.validateVat(memberStateCode, vatNumber)
                    .map(valid -> new VatValidation(memberStateCode, vatNumber, valid))
                    .blockingGet(); (5)
    }
}
1 Annotate the class with @FunctionBean. vies-vat-validator is an optional ID of the function which may or may not be used depending on the target platform.
2 We implement java.util.function.Function. That it is the easiest way to have a parameter with an incoming JSON Payload and JSON Payload response.
3 Configure a logger for the function. If you use AWS Lambda, you will be able to see this logs in CloudWatch Logs.
4 Inject via constructor VatService.
5 We cannot return a non-blocking type due to AWS Lambda Handler Input/Output supported Types.

Modify logback.xml to set DEBUG level for package example.micronaut.

vies-vat-validator/src/main/resources/logback.xml
<configuration>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoders are assigned the type
             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
    <logger name="example.micronaut" level="DEBUG"/>
</configuration>

2.4 Function Tests

build.gradle already contains these dependencies:

vies-vat-validator/build.gradle
    testRuntime "io.micronaut:micronaut-http-server-netty"
    testRuntime "io.micronaut:micronaut-function-web"

Because of that, testing your functions is really easy.

First modify ViesVatValidatorClient interface to match to the method signature forced by Function<VatValidationRequest, VatValidation>.

vies-vat-validator/src/test/java/example/micronaut/ViesVatValidatorClient.java
package example.micronaut;

import io.micronaut.function.client.FunctionClient;
import io.micronaut.http.annotation.Body;
import io.reactivex.Flowable;
import io.reactivex.Single;

import javax.inject.Named;

@FunctionClient
public interface ViesVatValidatorClient {

    @Named("vies-vat-validator")
    Single<VatValidation> apply(@Body VatValidationRequest request);

}

Now you can start up the Micronaut application and access your function via the client interface in your test.

Modify ViesVatValidatorFunctionTest.java:

vies-vat-validator/src/test/java/example/micronaut/ViesVatValidatorFunctionTest.java
package example.micronaut;

import io.micronaut.context.ApplicationContext;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class ViesVatValidatorFunctionTest {

    @Test
    public void testViesVatValidatorFunction() throws Exception {
        EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class);

        ViesVatValidatorClient client = server.getApplicationContext().getBean(ViesVatValidatorClient.class);

        VatValidationRequest req = new VatValidationRequest("es", "B99286353");
        assertTrue(client.apply(req).blockingGet().isValid());
        req = new VatValidationRequest("es", "B19280031");
        assertTrue(client.apply(req).blockingGet().isValid());
        req = new VatValidationRequest("es", "XXXXXXXXX");
        assertFalse(client.apply(req).blockingGet().isValid());

        server.stop();
    }
}

3 Test with SAM CLI

AWS SAM CLI is a CLI tool for local development and testing of Serverless applications.

You can skip this section. If you want to complete this section you will need SAM and Docker installed.

To test your Micronuat function with SAM you will first need to install SAM.

Then create a file event.json. We will use it as the stimulus for the SAM cli invocation.

vies-vat-validator/event.json
{
  "memberStateCode": "es",
  "vatNumber": "B86412491"
}

Next create a AWS SAM template file:

vies-vat-validator/template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
    Function:
        Timeout: 25

Resources:

    ViesVatValidatorFunction:
        Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
        Properties:
            CodeUri: build/libs/complete/vies-vat-validator-0.1-all.jar
            Handler: io.micronaut.function.aws.MicronautRequestStreamHandler
            Runtime: java8

With that in place, you can invoke your function locally:

$ sam local invoke "ViesVatValidatorFunction" -e event.json
2018-08-25 08:29:05 Invoking io.micronaut.function.aws.MicronautRequestStreamHandler (java8)
2018-08-25 08:29:05 Found credentials in shared credentials file: ~/.aws/credentials
2018-08-25 08:29:05 Decompressing /Users/sdelamo/Developer/micronaut-guides/micronaut-function-aws-lambda/complete/vies-vat-validator/build/libs/complete/vies-vat-validator-0.1-all.jar

Fetching lambci/lambda:java8 Docker container image......
2018-08-25 08:29:09 Mounting /private/var/folders/pb/dx1ms2_92_g54v9jvzv7k4hc0000gn/T/tmpvxc4Vb as /var/task:ro inside runtime container
START RequestId: e32a669c-bb51-4079-a1a1-781c2f144e4e Version: $LATEST
06:29:14.143 [main] DEBUG e.micronaut.ViesVatValidatorFunction - validate country: es vat number: B86412491
END RequestId: e32a669c-bb51-4079-a1a1-781c2f144e4e
REPORT RequestId: e32a669c-bb51-4079-a1a1-781c2f144e4e	Duration: 5627.70 ms	Billed Duration: 5700 ms	Memory Size: 128 MB	Max Memory Used: 9 MB

{"memberStateCode":"es","vatNumber":"B86412491","valid":true}

4 Deploy to AWS Lambda

Deploy your Micronaut function to AWS Lambda.

First, create a Function. I select Paris as the region.

step1

Select Java 8 runtime. Name: vies-vat-validator and create a new role form template(s). I give the role name lambda_basic_execution.

step2

Configure tests events:

step3

Create a payload with valid data:

step4

Generate a jar file:

`vies-vat-validator $ ./gradlew shadowJar`

The file is just 12MB ;-)

$ du -h build/libs/complete/vies-vat-validator-0.1-all.jar
 12M    build/libs/complete/vies-vat-validator-0.1-all.jar

Upload the JAR as illustrated here:

Enter as Handler value io.micronaut.function.aws.MicronautRequestStreamHandler.

I have allocated just 256Mb and a timeout of 25s.

step5

5 Writing the App

Now, it is time to consume the AWS Lambda function from another micronaut Microservice:

Create the microservice:

mn create-app example.micronaut.invoice

5.1 POJOs

We expect to get an incoming JSON payload such as:

{
"vatNumber": "B86412491",
"countryCode": "es",
"lines": [
    {
        "count": 2,
        "productId": "1491950358",
        "price": 19.99
    },
    {
        "count": 1,
        "productId": "1680502395",
        "price": 15
    },
]
}

We create two POJOs to map it.

invoice/src/main/java/example/micronaut/Invoice.java
package example.micronaut;

import java.util.List;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Objects;

public class Invoice {

    @NotNull
    @NotBlank
    private String vatNumber;

    @NotNull
    @NotBlank
    private String countryCode;

    @NotEmpty
    private List<InvoiceLine> lines;

    public Invoice() {

    }

    public Invoice(@NotNull @NotBlank String vatNumber,
                   @NotNull @NotBlank String countryCode,
                   @NotEmpty List<InvoiceLine> lines) {
        this.vatNumber = vatNumber;
        this.countryCode = countryCode;
        this.lines = lines;
    }

    public String getVatNumber() {
        return vatNumber;
    }

    public void setVatNumber(String vatNumber) {
        this.vatNumber = vatNumber;
    }

    public String getCountryCode() {
        return countryCode;
    }

    public void setCountryCode(String countryCode) {
        this.countryCode = countryCode;
    }

    public List<InvoiceLine> getLines() {
        return lines;
    }

    public void setLines(List<InvoiceLine> lines) {
        this.lines = lines;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Invoice invoice = (Invoice) o;
        return Objects.equals(vatNumber, invoice.vatNumber) &&
                Objects.equals(countryCode, invoice.countryCode) &&
                Objects.equals(lines, invoice.lines);
    }

    @Override
    public int hashCode() {
        return Objects.hash(vatNumber, countryCode, lines);
    }
}
invoice/src/main/java/example/micronaut/InvoiceLine.java
package example.micronaut;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import java.math.BigDecimal;
import java.util.Objects;

public class InvoiceLine {
    @NotNull
    @NotBlank
    private String productId;

    @Positive
    private Integer count;

    @NotNull
    @Positive
    private BigDecimal price;

    public InvoiceLine() {}

    public InvoiceLine(String productId, Integer count, BigDecimal price) {
        this.productId = productId;
        this.count = count;
        this.price = price;
    }

    public BigDecimal vatPrice(BigDecimal vatPercentage) {
        return getPrice().multiply(BigDecimal.valueOf(getCount()).multiply(vatPercentage));
    }

    public String getProductId() {
        return productId;
    }

    public void setProductId(String productId) {
        this.productId = productId;
    }

    public Integer getCount() {
        return count;
    }

    public void setCount(Integer count) {
        this.count = count;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        InvoiceLine that = (InvoiceLine) o;
        return Objects.equals(productId, that.productId) &&
                Objects.equals(count, that.count) &&
                Objects.equals(price, that.price);
    }

    @Override
    public int hashCode() {
        return Objects.hash(productId, count, price);
    }
}

5.2 Call the Function

Create an interface to abstract the collaboration with the function:

invoice/src/main/java/example/micronaut/VatValidator.java
package example.micronaut;

import io.micronaut.http.annotation.Body;
import io.reactivex.Single;

public interface VatValidator {

    Single<VatValidation> validateVat(@Body VatValidationRequest req);
}

Create two POJOs which are used by the previous interface:

invoice/src/main/java/example/micronaut/VatValidationRequest.java
package example.micronaut;

import java.io.Serializable;
import java.util.Objects;

public class VatValidationRequest implements Serializable {
    private String memberStateCode;
    private String vatNumber;

    public VatValidationRequest() {
    }

    public VatValidationRequest(String memberStateCode, String vatNumber) {
        this.memberStateCode = memberStateCode;
        this.vatNumber = vatNumber;
    }

    public String getMemberStateCode() {
        return memberStateCode;
    }

    public void setMemberStateCode(String memberStateCode) {
        this.memberStateCode = memberStateCode;
    }

    public String getVatNumber() {
        return vatNumber;
    }

    public void setVatNumber(String vatNumber) {
        this.vatNumber = vatNumber;
    }
}
invoice/src/main/java/example/micronaut/VatValidation.java
package example.micronaut;

import java.io.Serializable;
import java.util.Objects;

public class VatValidation extends VatValidationRequest implements Serializable {

    private Boolean valid;

    public VatValidation() {

    }

    public VatValidation(String memberStateCode, String vatNumber, Boolean valid) {
        super(memberStateCode, vatNumber);
        this.valid = valid;
    }

    public Boolean getValid() {
        return valid;
    }

    public void setValid(Boolean valid) {
        this.valid = valid;
    }

    public Boolean isValid() {
        return valid;
    }
}

Modify build.gradle to add function-client and com.amazonaws:aws-java-sdk-lambda dependencies:

invoice/build.gradle
    compile "io.micronaut:micronaut-function-client"
    runtime 'com.amazonaws:aws-java-sdk-lambda:1.11.285'

Also modify src/main/resources/application.yml and the define a function with the same name vies-vat-validator as the one we deployed to AWS Lambda:

invoice/src/main/resources/application.yml
aws:
    lambda:
        functions:
            vat:
                functionName: vies-vat-validator
                qualifer: vat
        region: eu-west-3 # Paris Region

Add the next configuration properties which we will use shortly:

invoice/src/main/resources/application.yml
vat:
    percentage: 0.21
    scale: 2
    retry:
        attempts: 5
        delay: 5s

Create VatClient to invoke our function:

invoice/src/main/java/example/micronaut/VatClient.java
package example.micronaut;

import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.function.client.FunctionClient;
import io.micronaut.http.annotation.Body;
import io.micronaut.retry.annotation.Retryable;
import io.reactivex.Single;

import javax.inject.Named;

@FunctionClient (1)
@Requires(notEnv = Environment.TEST) (2)
public interface VatClient extends VatValidator {

    @Override
    @Named("vies-vat-validator") (3)
    @Retryable(attempts = "${vat.retry.attempts:3}", delay = "${vat.retry.delay:1s}") (4)
    Single<VatValidation> validateVat(@Body VatValidationRequest req);
}
1 The FunctionClient annotation allows applying introduction advise to an interface such that methods defined by the interface become invokers of remote functions configured by the application.
2 You can remove a bean from the test classpath easily.
3 Use the function name vies-vat-validator
4 AWS Lambda functions may take time to warm up. Thus, failure is something you have to plan for and it is pretty common to want to attempt to retry an operation if it fails. With Micronaut Retry you can try again easily.

5.3 Controller

Micronaut’s validation is built on with the standard framework – JSR 380, also known as Bean Validation 2.0.

Hibernate Validator is a reference implementation of the validation API.

Add the next snippet to build.gradle

invoice/build.gradle
    compile "io.micronaut.configuration:micronaut-hibernate-validator"
invoice/src/main/java/example/micronaut/InvoiceController.java
package example.micronaut;

import io.micronaut.context.annotation.Value;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.validation.Validated;
import io.reactivex.Single;

import javax.validation.Valid;
import java.math.BigDecimal;

@Validated (1)
@Controller("/invoice") (2)
public class InvoiceController {

    private final BigDecimal vatPercentage;

    private final VatValidator vatValidator;

    private final int scale;

    public InvoiceController(@Value("${vat.percentage}") BigDecimal vatPercentage, (3)
                             @Value("${vat.scale}") int scale,
                             VatValidator vatValidator) { (4)
        this.vatPercentage = vatPercentage;
        this.scale = scale;
        this.vatValidator = vatValidator;
    }

    @Post("/vat") (5)
    Single<Taxes> calculateVat(@Valid @Body Invoice invoice) {  (6)
        return vatValidator.validateVat(new VatValidationRequest(invoice.getCountryCode(), invoice.getVatNumber())) (7)
                .map(vatValidation -> {
                        BigDecimal percentage = vatValidation.isValid() ? vatPercentage : new BigDecimal("0");
                    return new Taxes(invoice.getLines().stream()
                            .map(line -> line.vatPrice(percentage))
                            .reduce(BigDecimal.ZERO, BigDecimal::add)
                            .setScale(scale, BigDecimal.ROUND_HALF_EVEN));
                        });
    }
}
1 Add @Validated annotation at the class level to any class that requires validation.
2 The class is defined as a controller with the @Controller annotation mapped to the path /invoice.
3 Constructor injection of a configuration property.
4 Constructor injection of a bean.
5 The @Post annotation is used to map the calculateVat method to all requests to /invoice/vat that use an HTTP POST.
6 Add @Valid to any method parameter which requires validation. Use a POGO supplied as a JSON payload in the request to populate the invoice.
7 This invokes the function in AWS Lambda.

5.4 Tests

We don’t want the real AWS Lambda function executed in our tests.

Thus, create a bean to provide an implementation of VatValidator in the test phase:

invoice/src/test/java/example/micronaut/VatValidatorMock.java
package example.micronaut;

import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.reactivex.Single;

import javax.inject.Singleton;
import java.util.Collections;
import java.util.List;

@Requires(env = Environment.TEST)
@Singleton
public class VatValidatorMock implements VatValidator {

    @Override
    public Single<VatValidation> validateVat(VatValidationRequest request) {
        String memberStateCode = request.getMemberStateCode();
        String vatNumber = request.getVatNumber();
        List<String> validVatNumbers = Collections.singletonList("B84965375");
        return Single.just(new VatValidation(memberStateCode,
                vatNumber,
                validVatNumbers.contains(vatNumber)));
    }
}

Create a JUnit test which verifies the logic:

invoice/src/test/java/example/micronaut/InvoiceControllerTest.java
package example.micronaut;

import io.micronaut.context.ApplicationContext;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.RxHttpClient;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class InvoiceControllerTest {

    private static EmbeddedServer server;
    private static RxHttpClient rxHttpClient;

    @BeforeClass
    public static void setupServer() {
        server = ApplicationContext.run(EmbeddedServer.class); (1)
        rxHttpClient = server
                .getApplicationContext()
                .createBean(RxHttpClient.class, server.getURL()); (2)
    }

    @AfterClass
    public static void stopServer() {
        if (server != null) {
            server.stop();
        }
        if (rxHttpClient != null) {
            rxHttpClient.stop();
        }
    }

    @Test
    public void testBooksController() {

        VatValidator bean = server.getApplicationContext().getBean(VatValidator.class);
        assertTrue(bean instanceof VatValidatorMock); (3)

        List<InvoiceLine> lines = new ArrayList<InvoiceLine>();
        lines.add(new InvoiceLine("1491950358", 2, new BigDecimal(19.99)));
        lines.add(new InvoiceLine("1680502395", 1, new BigDecimal(15)));
        Invoice invoice = new Invoice("B84965375", "es", lines);
        HttpRequest request = HttpRequest.POST("/invoice/vat", invoice);
        Taxes rsp = rxHttpClient.toBlocking().retrieve(request, Taxes.class);
        BigDecimal expected = new BigDecimal("11.55");
        assertEquals(expected, rsp.getVat());


        invoice.setVatNumber("B99999999");
        rsp = rxHttpClient.toBlocking().retrieve(request, Taxes.class);
        expected = new BigDecimal("0.00");
        assertEquals(expected, rsp.getVat());
    }
}
1 To run the application from a unit test you can use the EmbeddedServer interface.
2 Register a RxHttpClient bean in the application context and point it to the embedded server URL. The EmbeddedServer interface provides the URL of the server under test which runs on a random port.
3 Verify the bean for interface VatValidator is VatValidatorMock.

If you are curious about how the 11.55 is calculated. Given a 21% VAT.

Price

Count

Line VAT

19,99

2

8,3958

15

1

3,15

Total VAT

11,55

6 Running the app

The easiest way to configure credentials for invoking the function is to define a ~/.aws/credentials` file`.

[default]
aws_access_key_id=XXXXXXXXXXX
aws_secret_access_key=XXXXX

To run the application use the ./gradlew run command which will start the application on a random port.

execute:

curl -X "POST" "http://localhost:8080/invoice/vat" \
     -H 'Content-Type: application/json' \
     -d $'{
  "countryCode": "es",
  "vatNumber": "B86412491",
  "lines": [
    {
      "count": 2,
      "price": 19.99,
      "productId": "1491950358"
    },
    {
      "count": 1,
      "price": 15,
      "productId": "1680502395"
    }
  ]
}'

and you will get:

{"vat":11.55}

7 AWS API Gateway

Amazon API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale.

A common use case of AWS API Gateway is to create HTTP endpoints which expose functions deploy to AWS Lambda.

Let’s create an endpoint which exposes the Micronaut function we deployed to AWS Lambda.

Create a new AWS Gateway API

awsapigateway1

Create a POST method, with integration type Lambda Function and enter the Lambda function name you used.

awsapigateway2

Create a Model for the incoming request to the method.

awsapigateway3

Edit the POST Method request and configure the Model as the content type.

awsapigateway4

You will be able to test the API method and its integration with the AWS Lambda function.

awsapigateway5

Deploy the API to create an endpoint which we can invoke with an HTTP request:

awsapigateway6
awsapigateway7

You will get an invoke URL to use:

awsapigateway8

7.1 Consume API Gateway URL

Although we are not using a discovery server like Consul or Eureka, you can manually configure service discovery in application.yml:

invoice/src/main/resources/application.yml
micronaut:
    http:
        services:
            viesvatvalidator: (1)
                urls:
                    - "https://8ah5p89xof.execute-api.eu-west-3.amazonaws.com"
                stage: "beta"
    application:
        name: invoice
    server:
        port: 8080
1 Configure service name, we will use name in @Client annotation.

Create a Micronaut declarative HTTP Client to consume that endpoint:

invoice/src/main/java/example/micronaut/AwsApiGatewayVatClient.java
package example.micronaut;

import io.micronaut.context.annotation.Primary;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.retry.annotation.Retryable;
import io.reactivex.Single;

@Primary (1)
@Client("viesvatvalidator") (2)
@Requires(notEnv = Environment.TEST) (3)
public interface AwsApiGatewayVatClient extends VatValidator {

    @Override
    @Retryable(attempts = "${vat.retry.attempts:3}", delay = "${vat.retry.delay:1s}")
    @Post("/${micronaut.http.services.viesvatvalidator.stage}") (4)
    Single<VatValidation> validateVat(@Body VatValidationRequest req);
}
1 Given a common interface called VatValidator that is implemented by multiple classes (AwsApiGatewayVatClient, VatClient) you can boost which bean is injection with @Primary.
2 Use the service id you defined in micronaut.http.services configuration.
3 You can remove a bean from the test classpath easily.
4 Define this as a POST request and interpolate a configuration property to define the path.

8 Running the Function on Graal Native Image

GraalVM is a new universal virtual machine from Oracle that supports a polyglot runtime environment and the ability to compile Java applications down to native machine code.

Any Micronaut application can be run using the GraalVM JVM, however special support has been added to Micronaut to support running Micronaut applications using GraalVM’s nativeimage tool.

We are going to modify the function project to be able to run it as Graal native image.

Create a copy of the function project.

$ cp -r vies-vat-validator vies-vat-validat-graal

Before the jar gradle task configure the archivesBaseName to be vies-vat-validator.

vies-vat-validator-graal/build.gradle
archivesBaseName = 'vies-vat-validator'
jar {
    manifest {
        attributes 'Main-Class': mainClassName
    }
}

The following steps were required:

1. Install Graal

sdk install java 1.0.0-rc7-graal
sdk use java 1.0.0-rc7-graal
You may need to be in the latest version of sdkman.io to see Graal as a java candidate.

2. Install the SVM Dependency

Once you have installed the SDK you should make the svm dependency available via your local Maven cache. The easiest way to do this is via Maven’s install command:

mvn install:install-file -Dfile=${JAVA_HOME}/jre/lib/svm/builder/svm.jar -DgroupId=com.oracle.substratevm -DartifactId=svm -Dversion=GraalVM-1.0.0-rc7 -Dpackaging=jar

3. Add Graal dependencies

Add the following to build.gradle:

vies-vat-validator/build.gradle
    compileOnly 'com.oracle.substratevm:svm:GraalVM-1.0.0-rc7'
    runtimeOnly "io.micronaut:micronaut-graal"

4. Add Graal Substitutions

Create src/main/java/example/micronaut/MicronautSubstitutions.java; a file needed to recompute Netty and Caffeine’s use of Unsafe.

vies-vat-validator-graal/src/main/java/example/micronaut/FunctionSubstitutions.java
package example.micronaut;

import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.RecomputeFieldValue;
import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind;
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;

import io.netty.util.internal.logging.InternalLoggerFactory;
import io.netty.util.internal.logging.JdkLoggerFactory;

@TargetClass(io.netty.util.internal.logging.InternalLoggerFactory.class)
final class Target_io_netty_util_internal_logging_InternalLoggerFactory {
    @Substitute
    private static InternalLoggerFactory newDefaultFactory(String name) {
        return JdkLoggerFactory.INSTANCE;
    }
}

@TargetClass(className = "io.micronaut.caffeine.cache.UnsafeRefArrayAccess")
final class Target_io_micronaut_caffeine_cache_UnsafeRefArrayAccess {
    @Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.ArrayIndexShift, declClass = Object[].class)
    public static int REF_ELEMENT_SHIFT;
}

@TargetClass(className = "io.netty.util.internal.PlatformDependent0")
final class Target_io_netty_util_internal_PlatformDependent0 {
    @Alias @RecomputeFieldValue(kind = Kind.FieldOffset, //
                    declClassName = "java.nio.Buffer", //
                    name = "address") //
    private static long ADDRESS_FIELD_OFFSET;
}

@TargetClass(className = "io.netty.util.internal.CleanerJava6")
final class Target_io_netty_util_internal_CleanerJava6 {
    @Alias @RecomputeFieldValue(kind = Kind.FieldOffset, //
                    declClassName = "java.nio.DirectByteBuffer", //
                    name = "cleaner") //
    private static long CLEANER_FIELD_OFFSET;
}

@TargetClass(className = "io.netty.util.internal.shaded.org.jctools.util.UnsafeRefArrayAccess")
final class Target_io_netty_util_internal_shaded_org_jctools_util_UnsafeRefArrayAccess {
    @Alias @RecomputeFieldValue(kind = Kind.ArrayIndexShift, declClass = Object[].class) //
    public static int REF_ELEMENT_SHIFT;
}

public class FunctionSubstitutions {
}

5. Build the Function

vies-vat-validator-graal $ ./gradlew assemble

6. Generate a reflect.json file

Run the next command to to write out a reflect.json; a file computing the classloading requirements of the application.

java -cp build/libs/vies-vat-validator-0.1-all.jar io.micronaut.graal.reflect.GraalClassLoadingAnalyzer

reflect.json file is written to destination: build/reflect.json

7. Build the Graal Native Image

Run the native-image command. Create a bash script to encapsulate the execution of this command:

vies-vat-validator-graal/build-native-image.sh
native-image \
    --no-server \
	--class-path build/libs/vies-vat-validator-0.1-all.jar \
	-H:ReflectionConfigurationFiles=build/reflect.json \
	-H:EnableURLProtocols=http \
	-H:IncludeResources="application.yml|META-INF/services/*.*" \
	-H:Name=vies-vat-validator \
	--delay-class-initialization-to-runtime=io.netty.handler.codec.http.HttpObjectEncoder \
	--rerun-class-initialization-at-runtime='sun.security.jca.JCAUtil$CachedSecureRandomHolder,javax.net.ssl.SSLContext' \
	-H:Class=io.micronaut.function.executor.FunctionApplication \
	-H:+ReportUnsupportedElementsAtRuntime \
	-H:+AllowVMInspection

vies-vat-validator-graal $ ./build-native-image.sh

8. Run the Native Function

The native function is now build. You can run it:

complete/vies-vat-validator-graal $ time echo '{"memberStateCode":"es", "vatNumber":"B99286353"}' | ./vies-vat-validator
10:22:30.003 [main] DEBUG e.micronaut.ViesVatValidatorFunction - validate country: es vat number: B99286353
{"valid":true,"memberStateCode":"es","vatNumber":"B99286353"}
real	0m1.475s
user	0m0.017s
sys	    0m0.034s
```

For comparision, execute with the jar file we generated in the previous section of this tutorial:

complete/vies-vat-validator $ time echo '{"memberStateCode":"es", "vatNumber":"B99286353"}' | java -jar build/libs/complete/vies-vat-validator-0.1-all.jar
10:25:52.136 [main] DEBUG e.micronaut.ViesVatValidatorFunction - validate country: es vat number: B99286353
{"memberStateCode":"es","vatNumber":"B99286353","valid":true}
real	0m3.261s
user	0m8.430s
sys	    0m0.723s

9 Next Steps

Read more about Serverless Functions inside Micronaut.

Learn more Micronaut for GraalVM integration.