Hills 🏔 and Skills, What's Common?

They both need you to be on top.

Cohort-1 just ended. You will get:

All yours, just at:

$99

Let’s build a QR Code Generator with AWS Lambda in Java

Updated on
java qr code gen banner

QRs are everywhere. Be it the floating Super Bowl ad by CoinBase to encourage user signups. Or to rickroll the entire internet.

It might look like just a black and white image — but in essence it’s powerful.

People say, Java developers live under a rock. But I’m sure that you’re not one of them. That’s why you crash-landed onto this article.

Let’s make something cool with AWS Lambda.

Java has never been that easy. But you’re going to have it easier.

Let’s start by creating an empty Java project!

We’ll be using Maven because of the popularity and ease of use. You can also use other package managers like Gradle or Groovy.

You don’t need to use any IDE, you can make this even in Notepad. Run the following command to create a boilerplate with Maven:

mvn archetype:generate -DgroupId=io.learnaws -DartifactId=qr-generator -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

Leave the other parameters as-is.

You’ll get an output in your terminal something like this:

[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------< org.apache.maven:standalone-pom >-------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] --------------------------------[ pom ]---------------------------------
[INFO] 
[INFO] >>> maven-archetype-plugin:3.2.1:generate (default-cli) > generate-sources @ standalone-pom >>>
[INFO] 
[INFO] <<< maven-archetype-plugin:3.2.1:generate (default-cli) < generate-sources @ standalone-pom <<<
[INFO] 
[INFO] 
[INFO] --- maven-archetype-plugin:3.2.1:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Batch mode
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Old (1.x) Archetype: maven-archetype-quickstart:1.0
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: basedir, Value: /home/shivam/learnaws
[INFO] Parameter: package, Value: io.learnaws
[INFO] Parameter: groupId, Value: io.learnaws
[INFO] Parameter: artifactId, Value: qr-generator
[INFO] Parameter: packageName, Value: io.learnaws
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] project created from Old (1.x) Archetype in dir: /home/shivam/learnaws/qr-generator
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.729 s
[INFO] Finished at: 2023-06-26T21:40:55+05:30
[INFO] ------------------------------------------------------------------------

Maven will create all the necessary files and directories depending on the package name similar to this:

├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── io
    │           └── learnaws
    │               └── App.java
    └── test
        └── java
            └── io
                └── learnaws
                    └── AppTest.java

Now we need to install AWS dependencies:

Simply add the AWS Lambda Core, Lambda Events dependencies and Maven Shade plugin in your pom.xml

We need them to easily interface our request and response, more on that later.

<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	....
    <!-- add maven compiler -->
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
	<!-- Add plugins inside the build -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.2.2</version>
        <configuration>
          <createDependencyReducedPom>false</createDependencyReducedPom>
        </configuration>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
		<!-- Your AWS deps goes here-->
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-core</artifactId>
      <version>1.2.2</version>
    </dependency>
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-events</artifactId>
      <version>3.11.0</version>
    </dependency>
  </dependencies>
</project>

Editing the main class file

Create your App.java’s function handleRequest like this:

package io.learnaws;

import java.util.HashMap;

// Import AWS dependencies
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse;

// We impement RequestHandler with API Gateway V2 cause Function URL uses the same
public class App implements RequestHandler<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> {
    @Override
    // Our main function name is handleRequest. We'll need this later.
    public APIGatewayV2HTTPResponse handleRequest(APIGatewayV2HTTPEvent event, Context context) {
        // instantiate a new APIGWV2 response
		APIGatewayV2HTTPResponse response = new APIGatewayV2HTTPResponse();
        // we are not encoding our content to base64 so we'll set it false
        response.setIsBase64Encoded(false);
        // your status code goes here eg: 200, 404, 401
        response.setStatusCode(200);
        // to set the headers we need a string to string hashmap
        HashMap<String, String> headers = new HashMap<String, String>();
        // if you don't set the content type it'll be default to application/json
        headers.put("Content-Type", "text/html");
        // finally set the headers in response
        response.setHeaders(headers);
        // Now finally set the body of the response as HTML string
        response.setBody("<h1>Hello from LearnAWS.io</h1>");
        // at last you just return the response from your handler
        return response;
    }
}

Bundling/Packaging the code for Lambda

Since it’s not JavaScript or Python we will have to compile it into a jar file so that AWS Lambda can run it in their runner.

Simply run mvn package , a jar file will be created at /src/main/resources.

[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------------< io.learnaws:qr-generator >----------------------
[INFO] Building qr-generator 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ qr-generator ---
[INFO] skip non existing resourceDirectory /home/shivam/learnaws/java-cdk/qr-generator/src/main/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ qr-generator ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ qr-generator ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /home/shivam/learnaws/java-cdk/qr-generator/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ qr-generator ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ qr-generator ---
[INFO] Surefire report directory: /home/shivam/learnaws/java-cdk/qr-generator/target/surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running io.learnaws.AppTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.006 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] 
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ qr-generator ---
[INFO] 
[INFO] --- maven-shade-plugin:3.2.2:shade (default) @ qr-generator ---
[INFO] Including com.amazonaws:aws-lambda-java-core:jar:1.2.2 in the shaded jar.
[INFO] Including com.amazonaws:aws-lambda-java-events:jar:3.11.0 in the shaded jar.
[INFO] Including joda-time:joda-time:jar:2.6 in the shaded jar.
[INFO] Including io.nayuki:qrcodegen:jar:1.8.0 in the shaded jar.
[WARNING] Discovered module-info.class. Shading will break its strong encapsulation.
[INFO] Replacing original artifact with shaded artifact.
[INFO] Replacing /home/shivam/learnaws/java-cdk/qr-generator/target/qr-generator-1.0-SNAPSHOT.jar with /home/shivam/learnaws/java-cdk/qr-generator/target/qr-generator-1.0-SNAPSHOT-shaded.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.904 s
[INFO] Finished at: 2023-07-13T16:14:22+05:30
[INFO] ------------------------------------------------------------------------

Create a Java 17 Lambda function

To create a new function (console)

  1. Open the Functions page of the Lambda console and choose Create Function.
creating java lambda function advanced settings for java lambda function url

You can follow this guide from AWS if you want to use groovy.

Upload the jar from Lambda console

uploading jar file from lambda console

Select the Upload from → .zip or .jar file.

Now drag and drop your .jar from inside your target folder. (NOT the one which says original)

target folder structure for jar file uploading jar file to lambda

You’ll be greeted with the following message in a green banner:

Successfully updated the function lambda-qr-gen.

Update your handler

Updating your handler ensures that AWS Lambda finds where your function is located and serves exactly that.

Go to the Runtime Settings section and click on the Edit button.

In the Handler, change example.Handler to your groupId.artifactId::functionName. It would look like com.package_name.FileName::yourHandlerFunction.

Hit Save.

edit runtime settings to add lambda handler name

Boost performance by enabling SnapStart (optional)

You can reduce the startup time of your Lambda at no cost.

Go to Configuration tab and click the Edit button.

Change the value of SnapStart from None to PublishedVersions from the drop-down menu.

Hit Save.

enable snapstart for published versions in lambda function

Enjoy a faster startup experience as your Lambda snaps back into action from memory instead of HDD.

Testing your Lambda function

If you expand the Function overview, on the right you’ll see the Function URL. Just click on it.

lambda function overview containing url and arn

Et voila! You’ll get to see the text “Hello from LearnAWS” or whatever you put in your body on your screen.

You just deployed your first Lambda function. Give yourself a pat 🐾You worked hard for this.

hello for learnaws printed on visiting lambda function url

Let’s get your QR code up and running!

I found the best QR Code generator library for Java.

It’s also available in 6 other languages (JavaScript, TypeScript, Python, Rust, C++ and C).

This QR Code generator library is made by Nayuki, kudos to the amazing functions and example she wrote. Leave a start on GitHub: https://github.com/nayuki/QR-Code-generator

Add this as a dependency to your pom.xml:

<dependency>
    <groupId>io.nayuki</groupId>
    <artifactId>qrcodegen</artifactId>
    <version>1.8.0</version>
</dependency>

Add QR code generator helper methods

Create QrUtils.java

Just like you have App.java, create another file called QrUtils.java.

This will contain our helper functions needed to generate the QR code using the qrcodegen library.

Taken from https://github.com/nayuki/QR-Code-generator/blob/master/java/QrCodeGeneratorDemo.java

package example;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Objects;

import javax.imageio.ImageIO;

import io.nayuki.qrcodegen.QrCode;

public class QrUtils {
    /*---- Utilities ----*/

    public static BufferedImage toImage(QrCode qr, int scale, int border) {
        return toImage(qr, scale, border, 0xFFFFFF, 0x000000);
    }

    /**
     * Returns a raster image depicting the specified QR Code, with
     * the specified module scale, border modules, and module colors.
     * <p>
     * For example, scale=10 and border=4 means to pad the QR Code with 4 light
     * border
     * modules on all four sides, and use 10&#xD7;10 pixels to represent each
     * module.
     * 
     * @param qr         the QR Code to render (not {@code null})
     * @param scale      the side length (measured in pixels, must be positive) of
     *                   each module
     * @param border     the number of border modules to add, which must be
     *                   non-negative
     * @param lightColor the color to use for light modules, in 0xRRGGBB format
     * @param darkColor  the color to use for dark modules, in 0xRRGGBB format
     * @return a new image representing the QR Code, with padding and scaling
     * @throws NullPointerException     if the QR Code is {@code null}
     * @throws IllegalArgumentException if the scale or border is out of range, or
     *                                  if
     *                                  {scale, border, size} cause the image
     *                                  dimensions to exceed Integer.MAX_VALUE
     */
    public static BufferedImage toImage(QrCode qr, int scale, int border, int lightColor, int darkColor) {
        Objects.requireNonNull(qr);
        if (scale <= 0 || border < 0)
            throw new IllegalArgumentException("Value out of range");
        if (border > Integer.MAX_VALUE / 2 || qr.size + border * 2L > Integer.MAX_VALUE / scale)
            throw new IllegalArgumentException("Scale or border too large");

        BufferedImage result = new BufferedImage((qr.size + border * 2) * scale, (qr.size + border * 2) * scale,
                BufferedImage.TYPE_INT_RGB);
        for (int y = 0; y < result.getHeight(); y++) {
            for (int x = 0; x < result.getWidth(); x++) {
                boolean color = qr.getModule(x / scale - border, y / scale - border);
                result.setRGB(x, y, color ? darkColor : lightColor);
            }
        }
        return result;
    }

    // Helper function to reduce code duplication.
    public static void writePng(BufferedImage img, String filepath) throws IOException {
        ImageIO.write(img, "png", new File(filepath));
    }

    /**
     * Returns a string of SVG code for an image depicting the specified QR Code,
     * with the specified
     * number of border modules. The string always uses Unix newlines (\n),
     * regardless of the platform.
     * 
     * @param qr         the QR Code to render (not {@code null})
     * @param border     the number of border modules to add, which must be
     *                   non-negative
     * @param lightColor the color to use for light modules, in any format supported
     *                   by CSS, not {@code null}
     * @param darkColor  the color to use for dark modules, in any format supported
     *                   by CSS, not {@code null}
     * @return a string representing the QR Code as an SVG XML document
     * @throws NullPointerException     if any object is {@code null}
     * @throws IllegalArgumentException if the border is negative
     */
    public static String toSvgString(QrCode qr, int border, String lightColor, String darkColor) {
        Objects.requireNonNull(qr);
        Objects.requireNonNull(lightColor);
        Objects.requireNonNull(darkColor);
        if (border < 0)
            throw new IllegalArgumentException("Border must be non-negative");
        long brd = border;
        StringBuilder sb = new StringBuilder()
                .append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
                .append("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
                .append(String.format(
                        "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 %1$d %1$d\" stroke=\"none\">\n",
                        qr.size + brd * 2))
                .append("\t<rect width=\"100%\" height=\"100%\" fill=\"" + lightColor + "\"/>\n")
                .append("\t<path d=\"");
        for (int y = 0; y < qr.size; y++) {
            for (int x = 0; x < qr.size; x++) {
                if (qr.getModule(x, y)) {
                    if (x != 0 || y != 0)
                        sb.append(" ");
                    sb.append(String.format("M%d,%dh1v1h-1z", x + brd, y + brd));
                }
            }
        }
        return sb
                .append("\" fill=\"" + darkColor + "\"/>\n")
                .append("</svg>\n")
                .toString();
    }
}

Get the route path from API Request

// Add a default QR text
String qrText = "Add your text after the /";
// Remove the first slash (/) from the path
String routePath = event.getRawPath().substring(1);

// Update the QR text if route is not empty
if (routePath.trim() != "") {
	// Decode the URI to look like 'cute bird' instead of 'cute%20bird'
  qrText = URLDecoder.decode(routePath, StandardCharsets.UTF_8);
}

Generate and update the response body with SVG QR

try {
	// first we encode the text by passing text and error correction
  QrCode qr0 = QrCode.encodeText(qrText, QrCode.Ecc.MEDIUM);
	// using the utils we convert it to SVG or PNG
  String svg = QrUtils.toSvgString(qr0, 2, "#ffffff", "#000000");
	// set the SVG or png as string in response body
  response.setBody(svg);
} catch (Exception e) {
  e.printStackTrace();
}

Final code looks:

package io.learnaws;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;

import javax.imageio.ImageIO;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse;

import io.nayuki.qrcodegen.QrCode;

public class GenerateQr implements RequestHandler<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> {
  @Override
  public APIGatewayV2HTTPResponse handleRequest(APIGatewayV2HTTPEvent event, Context context) {

    APIGatewayV2HTTPResponse response = new APIGatewayV2HTTPResponse();

    String qrText = "Add your text after the /";
    String routePath = event.getRawPath().substring(1);

    if (routePath.trim() != "") {
      qrText = URLDecoder.decode(routePath, StandardCharsets.UTF_8);
    }
    try {
        QrCode qr0 = QrCode.encodeText(qrText, QrCode.Ecc.MEDIUM);
        String svg = QrUtils.toSvgString(qr0, 2, "#ffffff", "#000000");
        response.setBody(svg);
        response.setIsBase64Encoded(false);
        response.setStatusCode(200);
        HashMap<String, String> headers = new HashMap<String, String>();
        headers.put("Content-Type", "image/svg+xml");
        response.setHeaders(headers);

    } catch (Exception e) {
      e.printStackTrace();
    }
    return response;
  }

}

Run mvn package & upload a new JDK

Now we repeat the same step which we did earlier for bundling and uploading the .jar file to AWS Lambda console.

Get the complete example and code

Complete source code including PNG generation example on this GitHub repo.

Let’s put our QR to the test 🧪

Final test by visiting the same {function URL}/your qr text which you got earlier. If you don’t add anything after / you’ll see the default text.

Open your camera and simply point it towards the QR:

opening camera to scan qr code created by lambda function