Como criei um gerador de QR Code com Java, Spring, Docker e AWS (e por que a tecnologia é essencial)

Imagem de perfil de Fernanda Kipper

Fernanda KipperAutor

Hoje quero compartilhar com vocês uma experiência prática e muito valiosa que tive recentemente: criar um gerador de QR Code utilizando Java, Spring Boot, Docker e a AWS S3. Vou explicar o passo a passo do projeto e também refletir um pouco sobre a importância de aplicarmos tecnologias modernas no nosso dia a dia de desenvolvimento.

Este projeto está disponível no meu GitHub, caso você queira acompanhar ou clonar:
Projeto no GitHub

1. Criando o projeto inicial

Comecei utilizando o Spring Initializer, uma ferramenta que acelera (e muito) a criação de projetos Java. Preenchi informações básicas como Group, Artifact, Name, Java Version (21) e selecionei Maven como o gerenciador de pacotes. Depois, adicionei duas dependências principais: Spring Web e Spring Boot DevTools.

Esse passo parece simples, mas mostra o quanto a tecnologia evoluiu para nos poupar tempo. Antes, configurar um projeto Java do zero era extremamente manual e sujeito a erros. Hoje, temos plataformas que geram a estrutura base em poucos cliques.

2. Configuração do projeto no IntelliJ IDEA

Agora, com o projeto criado, adicionei duas bibliotecas essenciais no pom.xml:

  • ZXing para gerar QR Codes;
  • AWS SDK para enviar arquivos para o S3.

<dependency>
	<groupId>software.amazon.awssdk</groupId>
	<artifactId>s3</artifactId>
	<version>${aws.sdk.version}</version>
</dependency>

<dependency>
	<groupId>com.google.zxing</groupId>
	<artifactId>core</artifactId>
	<version>${google.zxing.vesion}</version>
</dependency>

<dependency>
	<groupId>com.google.zxing</groupId>
	<artifactId>javase</artifactId>
	<version>${google.zxing.vesion}</version>
</dependency>

Recomendo utilizar a prática de definir as versões das bibliotecas no bloco <properties>, o que facilita futuras atualizações. Essa atenção aos detalhes ajuda muito na manutenção do projeto, especialmente em ambientes profissionais.

<properties>
	<java.version>21</java.version>
	<google.zxing.vesion>3.5.2</google.zxing.vesion>
	<aws.sdk.version>2.24.12</aws.sdk.version>
</properties>

Leia esse passo a passo completo para entender como configurar a AWS no IntelliJ

3. Organização da Estrutura do Projeto

A estrutura do projeto é algo que não abro mão: código organizado é código saudável. Organizei os pacotes da seguinte forma:

  • controller (para receber requisições),
  • dto (para transportar dados),
  • ports (para interfaces de comunicação),
  • infrastructure (para implementações, como o S3 da AWS).

Essa separação foi inspirada na arquitetura hexagonal, que vem ganhando espaço na comunidade de desenvolvimento, porém com algumas adaptações de outros padrões.

4. Desenvolvimento da Aplicação

Aqui começa a parte mais divertida

Criando o Controller

Implementei o QrCodeController para ser a porta de entrada das requisições HTTP. Ele recebe os dados, chama o serviço e devolve a URL do QR Code gerado.

package com.fernandakipper.qrcode.generator.controller;


import com.fernandakipper.qrcode.generator.dto.QrCodeGenerateRequest;
import com.fernandakipper.qrcode.generator.dto.QrCodeGenerateResponse;
import com.fernandakipper.qrcode.generator.service.QrCodeGeneratorService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/qrcode")
public class QrCodeController {

    private final QrCodeGeneratorService qrCodeGeneratorService;

    public QrCodeController(QrCodeGeneratorService qrCodeService) {
        this.qrCodeGeneratorService = qrCodeService;
    }

    @PostMapping
    public ResponseEntity<QrCodeGenerateResponse> generate(@RequestBody QrCodeGenerateRequest request){
        try {
            QrCodeGenerateResponse response = this.qrCodeGeneratorService.generateAndUploadQrCode(request.text());
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            System.out.println(e);
            return ResponseEntity.internalServerError().build();
        }
    }
}

Criando os DTOs

Criei dois objetos simples para transportar os dados: QrCodeGenerateRequest e QrCodeGenerateResponse.

package com.fernandakipper.qrcode.generator.dto;

public record QrCodeGenerateRequest(String text) {
}

package com.fernandakipper.qrcode.generator.dto;

public record QrCodeGenerateResponse(String text) {
}

No Java é muito interessante usarmos o tipo record para criar nossas classes de DTOs, pois são dados imutáveis e só iremos usar esse objeto para transportar informações, e o record simplifica a criação dessas classes ao gerar automaticamente construtor, getters, equals, hashCode e toString, reduzindo a verbosidade do código.

Criando a interface StoragePort

Implementei o padrão de portas e adaptadores. Defini a interface StoragePort como uma abstração para o serviço de armazenamento. Isso me permite, no futuro, trocar a AWS S3 por outro serviço sem mexer na lógica principal da aplicação.

package com.fernandakipper.qrcode.generator.ports;

public interface StoragePort {
    String uploadFile(byte[] fileData, String fileName, String contentType);
}

Implementando com AWS S3

A implementação S3StorageAdapter conectou a aplicação ao meu bucket no S3, utilizando o SDK da AWS. 

package com.fernandakipper.qrcode.generator.infrastructure;

import com.fernandakipper.qrcode.generator.ports.StoragePort;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

@Component
public class S3StorageAdapter implements StoragePort {

    private final S3Client s3Client;
    private final String bucketName;
    private final String region;


    public S3StorageAdapter(@Value("${aws.s3.region}") String region,
                            @Value("${aws.s3.bucket-name}") String bucketName) {
        this.bucketName = bucketName;
        this.region = region;
        this.s3Client = S3Client.builder()
                .region(Region.of(this.region))
                .build();
    }

    @Override
    public String uploadFile(byte[] fileData, String fileName, String contentType) {
        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(fileName)
                .contentType(contentType)
                .build();

        s3Client.putObject(putObjectRequest, RequestBody.fromBytes(fileData));

        return String.format("https://%s.s3.%s.amazonaws.com/%s",
                bucketName, region, fileName);
    }
}

Serviço de Geração de QR Code

Utilizei a biblioteca ZXing para gerar a imagem do QR Code a partir do texto enviado, converti a imagem para bytes e fiz o upload no S3. Em retorno, o S3 nos devolve o ID daquele item no bucket, e então usei o ID para montar uma URL pública do QR Code gerado.

package com.fernandakipper.qrcode.generator.service;

import com.fernandakipper.qrcode.generator.dto.QrCodeGenerateResponse;
import com.fernandakipper.qrcode.generator.ports.StoragePort;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.UUID;

@Service
public class QrCodeGeneratorService {
    private final StoragePort storage;

    public QrCodeGeneratorService(StoragePort storage) {
        this.storage = storage;
    }

    public QrCodeGenerateResponse generateAndUploadQrCode(String text) throws WriterException, IOException {
        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, 200, 200);

        ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
        MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
        byte[] pngQrCodeData = pngOutputStream.toByteArray();

        String url = storage.uploadFile(pngQrCodeData, UUID.randomUUID().toString(), "image/png");

        return new QrCodeGenerateResponse(url);
    }
}

Criação do bucket e política de acesso público

Para isso tudo funcionar, precisamos criar nosso bucket no S3 e garantir que os objetos dentro dele sejam de acesso pública, dessa maneira a URL de acesso publico que montei no service vai funcionar.
Para criar usei todas configurações padrões do S3, mudando apenas a parte de “Bloquear todo acesso público”
Imagem do post

Por padrão, a AWS priva os objetos de um bucket e mesmo com a opção de bloquear acesso público deselecionada, para que eles sejam de livre leitura através de uma URL pública precisamos adicionar uma política no nosso bucket

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::qr-code-generator-bucket/*"
    }
  ]
}

5. Empacotando Tudo com Docker

Para finalizar, criei um Dockerfile que empacota a aplicação em um container, o que permite que ela seja executada em qualquer ambiente de forma previsível.
Se quiser ainda mais facilidade, você pode usar um docker-compose.yml para orquestrar tudo.

O principal ponto aqui é as variáveis de ambiente da AWS que são recebidas como parâmetros no momento de buildar a imagem, e o nome e região do bucket já deixei declarado no próprio Dockerfile pois não são dados sensíveis.

FROM maven:3.9.6-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar

ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY

ENV AWS_REGION=us-east-1
ENV AWS_BUCKET_NAME=qrcode-storager

ENTRYPOINT ["java", "-jar", "app.jar"]

Um ponto importante ao fazer o deploy dessa aplicação na própria AWS é que, na maioria dos casos, não é necessário injetar manualmente as variáveis de AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY. Em vez disso, você pode atribuir permissões diretamente ao serviço da AWS que estará executando sua aplicação — como uma instância EC2, um container no ECS ou uma função Lambda — por meio de IAM Roles (funções do IAM).

Por que tudo isso importa?

Investir em aprendizado de novas tecnologias não é mais um diferencial: é uma necessidade.
Novos conhecimentos não só nós mantêm relevantes em um mercado tão competitivo, como também nos dão a liberdade de inovar, testar novas ideias e criar produtos que realmente geram valor.

E aí, ficou com vontade de criar seu próprio gerador de QR Code?
Se precisar de ajuda ou quiser compartilhar sua experiência, estou sempre disponível para conversar!

Até a próxima!