📚 Pré-requisitos: É fundamental ter concluído a Parte 01 — API REST com Spring Boot e a Parte 02 — Cliente Web com Svelte (JavaScript), ou ter a API Spring Boot em execução na porta
8080.
Bem-vindo à Parte 03 da Trilha Prática de Aplicações Full Stack!
Na Parte 01, você construiu a API REST central com Spring Boot e banco H2. Na Parte 02, conectamos o primeiro cliente desacoplado: uma Single Page Application em Svelte (JavaScript puro) rodando no navegador na porta 5173.
Agora, daremos um passo fundamental na compreensão de arquiteturas corporativas modernas: construiremos um Cliente Desktop Clássico com Java Swing que consome a mesmíssima API REST.
Trilha Prática de Aplicações Full Stack:
├── Parte 01: API REST com Spring Boot + YARC + Thymeleaf (SSR) [Concluído]
├── Parte 02: Cliente Web Desacoplado com Svelte (JS Puro) [Concluído]
├── Parte 03: Cliente Desktop Clássico com Java Swing + Multi-Cliente [ESTE PROJETO]
├── Parte 04: Cliente Mobile Nativo com Android (Kotlin)
├── Parte 05 a 08: Docker, Arquitetura, Vue.js, React e Testes
└── Parte 09: Evolução Arquitetural Desktop: De Swing para JavaFX Moderno
Para quem está tendo o primeiro contato com aplicações desktop que consom APIs, o Java Swing (javax.swing.*) oferece a melhor curva de aprendizado:
module-info.java, flags especiais de máquina virtual (--add-modules), nem plugins pesados no Maven.GET, POST, PUT, DELETE).No ambiente corporativo, operadores de escritório frequentemente preferem aplicativos desktop nativos por permitirem digitação contínua, atalhos de teclado e estabilidade sem depender de abas de navegador:
| ID | Nome do Requisito | Descrição Detalhada | Regra de Negócio / UX |
|---|---|---|---|
| RF-D01 | Carregar Dados na Tabela | Ao abrir o aplicativo, disparar GET /api/tarefas e preencher o JTable. |
Exibir indicador na barra de status: 🟢 Conectado ou 🔴 Desconectado. |
| RF-D02 | Cadastrar Nova Tarefa | Coletar o texto do JTextField, montar o objeto JSON e enviar via POST /api/tarefas. |
Impedir envio de campos vazios. Limpar o campo de texto após o sucesso. |
| RF-D03 | Alternar Conclusão | Inverter o status da tarefa selecionada e enviar via PUT /api/tarefas/{id}. |
Exigir que uma linha da tabela esteja previamente selecionada. |
| RF-D04 | Editar Tarefa | Abrir caixa de diálogo (JOptionPane) com a descrição atual e atualizar via PUT. |
Não permitir salvar descrição vazia. |
| RF-D05 | Excluir Tarefa | Disparar DELETE /api/tarefas/{id} e remover a linha da tabela após confirmação. |
Exibir confirmação (JOptionPane.showConfirmDialog). |
| RF-D06 | Sincronização Manual | Fornecer botão para recarregar a lista da API a qualquer momento. | Garante que dados inseridos pelo Svelte, Thymeleaf ou YARC apareçam imediatamente na tela. |
| ID | Categoria | Descrição do Critério | Justificativa Técnica |
|---|---|---|---|
| RNF-D01 | Interface Nativa (Swing) | A tela deve ser construída com javax.swing.* e Look and Feel nativo do sistema operacional. |
Padrão integrado ao Java SE, sem necessidade de módulos externos ou plugins adicionais. |
| RNF-D02 | HTTP Client Nativo do Java | A comunicação com a API deve utilizar java.net.http.HttpClient nativo do Java 11+. |
Elimina a necessidade de bibliotecas externas pesadas para transporte HTTP. |
| RNF-D03 | Serialização JSON com Jackson | A conversão entre JSON e objetos Java deve ser realizada pelo ObjectMapper do Jackson. |
Padrão corporativo para alto desempenho e total compatibilidade com o Spring Boot. |
| RNF-D04 | Thread Safety na UI (EDT) | Requisições HTTP em segundo plano (CompletableFuture) não podem travar a Event Dispatch Thread. |
Evita que a janela congele enquanto aguarda resposta da rede. |
| RNF-D05 | Executável Portável (Fat JAR) | A aplicação deve ser empacotada em um único arquivo .jar contendo todas as dependências. |
Facilita a distribuição e execução com um duplo clique ou comando java -jar. |
flowchart LR
UserWeb(["👤 Usuário Web (Svelte)"])
UserDesktop(["👤 Operador Desktop (Swing)"])
subgraph CasosDeUso ["🎯 Casos de Uso Canônicos do Sistema"]
direction TB
UC01(["UC01: Consultar Tarefas"])
UC02(["UC02: Cadastrar Nova Tarefa"])
UC03(["UC03: Alternar Status Concluído"])
UC04(["UC04: Editar Descrição"])
UC05(["UC05: Excluir Tarefa"])
end
subgraph BackendCentral ["⚙️ API REST Spring Boot (Porta 8080) — SSOT"]
direction TB
Endpoint["🔌 Endpoints /api/tarefas (JSON)"]
Service["🧠 TarefaService (Regras de Negócio)"]
DB[("💾 Banco H2 em Memória")]
Endpoint --> Service
Service --> DB
end
UserWeb --> UC01 & UC02 & UC03 & UC04 & UC05
UserDesktop --> UC01 & UC02 & UC03 & UC04 & UC05
UC01 & UC02 & UC03 & UC04 & UC05 <-->|HTTP REST / JSON| Endpoint
style CasosDeUso fill:#f8fafc,stroke:#334155,stroke-width:2px
style BackendCentral fill:#f0fdf4,stroke:#16a34a,stroke-width:2px
| Caso de Uso | Descrição | Requisitos Atendidos | Disparo na UI Desktop |
|---|---|---|---|
| UC01 | Carregar e exibir tabela de tarefas em tempo real | RF-D01, RF-D06 | Inicialização da tela ou botão 🔄 Atualizar |
| UC02 | Inserir nova tarefa no banco de dados central | RF-D02 | Campo de texto + botão ➕ Adicionar ou Enter |
| UC03 | Alternar status entre Pendente e Concluída | RF-D03 | Linha selecionada + botão ✔️ Concluir / Reabrir |
| UC04 | Modificar descrição da tarefa existente | RF-D04 | Duplo clique na linha ou botão ✏️ Editar |
| UC05 | Deletar tarefa permanentemente | RF-D05 | Linha selecionada + botão 🗑️ Excluir |
A tabela abaixo demonstra como a mesma API REST atende os mesmos requisitos funcionais tanto na Web (Svelte) quanto no Desktop (Swing):
| Requisito | No Cliente Web Svelte (Parte 02) | No Cliente Desktop Swing (Parte 03) | Endpoint da API REST | Status HTTP |
|---|---|---|---|---|
| Listar Tarefas | api.js: listarTarefas() |
tarefaApiService.listarTarefas() |
GET /api/tarefas |
200 OK |
| Criar Tarefa | api.js: criarTarefa() |
tarefaApiService.criarTarefa() |
POST /api/tarefas |
201 Created |
| Alternar Status | api.js: atualizarTarefa() |
tarefaApiService.alternarStatus() |
PUT /api/tarefas/{id} |
200 OK |
| Editar Descrição | Edição Inline no DOM (dblclick) |
tarefaApiService.atualizarDescricao() |
PUT /api/tarefas/{id} |
200 OK |
| Excluir Tarefa | api.js: excluirTarefa() |
tarefaApiService.excluirTarefa() |
DELETE /api/tarefas/{id} |
204 No Content |
| Estrutura de Tela | Componentes Svelte 5 (.svelte) |
JFrame + JTable + JPanel (Swing) |
— | — |
| Biblioteca HTTP | fetch nativo do Browser ES6+ |
Java java.net.http.HttpClient |
— | — |
Observe com atenção o diagrama abaixo. Ele sintetiza o grande marco da arquitetura de software: o desacoplamento total entre frontends e backends.
TB_TAREFAS do H2 e fica imediatamente disponível para a aplicação Angular e para a interface Thymeleaf.Na arquitetura corporativa deste curso, a API REST reside na pasta canônica fullstack_tarefas_01_spring_api/api-spring.
Antes de abrir o projeto Desktop, certifique-se de que a API central está ativa:
cd ../fullstack_tarefas_01_spring_api/api-spring
.\mvnw.cmd spring-boot:run
./mvnw spring-boot:run
http://localhost:8080/api/tarefas.🎯 Checkpoint de Verificação 1: Backend Ativo
- Como validar agora: Abra o navegador ou YARC e acerte
GET http://localhost:8080/api/tarefas. Deve retornar status200 OK.
pom.xml)O projeto desktop-swing é gerenciado pelo Maven de forma limpa, exigindo apenas o Jackson para conversão JSON:
desktop-swing/
├── pom.xml ← Configurações do Maven e Jackson
├── mvnw ← Script Maven para Linux/macOS
├── mvnw.cmd ← Script Maven para Windows
└── src/
└── main/
└── java/
└── br/com/curso/listadetarefas/desktop/
├── Tarefa.java ← Modelo POJO compatível com Jackson
├── TarefaApiService.java ← Cliente HTTP nativo (java.net.http)
├── TarefaSwingFrame.java ← Janela principal JFrame com JTable e ações
└── MainSwingApp.java ← Ponto de entrada com Look and Feel nativo
pom.xml<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>br.com.curso</groupId>
<artifactId>desktop-swing</artifactId>
<version>1.0-SNAPSHOT</version>
<name>desktop-swing</name>
<description>Cliente Desktop Clássico em Java Swing para a Lista de Tarefas</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<jackson.version>2.17.2</jackson.version>
</properties>
<dependencies>
<!-- Jackson Databind para serialização e desserialização de JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Plugin de Compilação Java SE Padrão -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<!-- Plugin Shade para empacotar um JAR executável completo com dependências (Fat JAR) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>br.com.curso.listadetarefas.desktop.MainSwingApp</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
🎯 Checkpoint de Verificação 2: Dependências e Compilação do Maven
- Como validar agora: Abra o terminal na pasta
desktop-swinge execute:.\mvnw.cmd compileDeve retornar
[INFO] BUILD SUCCESS.
Tarefa.java)No cliente desktop, criamos um modelo POJO limpo compatível com a desserialização do Jackson:
src/main/java/br/com/curso/listadetarefas/desktop/Tarefa.javapackage br.com.curso.listadetarefas.desktop;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
/**
* Modelo de dados que representa uma Tarefa no cliente Desktop Swing.
* Compatível com o payload JSON da API REST Spring Boot (/api/tarefas).
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class Tarefa {
private Long id;
private String descricao;
private boolean concluida;
public Tarefa() {
}
public Tarefa(Long id, String descricao, boolean concluida) {
this.id = id;
this.descricao = descricao;
this.concluida = concluida;
}
public Tarefa(String descricao) {
this(null, descricao, false);
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getDescricao() {
return descricao;
}
public void setDescricao(String descricao) {
this.descricao = descricao;
}
public boolean isConcluida() {
return concluida;
}
public void setConcluida(boolean concluida) {
this.concluida = concluida;
}
@Override
public String toString() {
return String.format("[%s] #%d - %s", (concluida ? "X" : " "), id != null ? id : 0, descricao);
}
}
TarefaApiService.java)Esta classe é o coração da comunicação de rede no Desktop. Ela utiliza a biblioteca nativa java.net.http.HttpClient do Java 11+ para executar as quatro operações do CRUD:
src/main/java/br/com/curso/listadetarefas/desktop/TarefaApiService.javapackage br.com.curso.listadetarefas.desktop;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
/**
* Camada de serviço de rede responsável por consumir a API REST Spring Boot.
* Utiliza o java.net.http.HttpClient nativo do Java 11+ e Jackson para JSON.
*/
public class TarefaApiService {
private final String apiUrl;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
public TarefaApiService() {
this("http://localhost:8080/api/tarefas");
}
public TarefaApiService(String apiUrl) {
this.apiUrl = apiUrl;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
this.objectMapper = new ObjectMapper();
}
// RF-D01: Listar todas as tarefas cadastradas (GET /api/tarefas)
public List<Tarefa> listarTarefas() throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(5))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
return objectMapper.readValue(response.body(), new TypeReference<List<Tarefa>>() {});
} else {
throw new IOException("Falha ao listar tarefas. Código HTTP: " + response.statusCode());
}
}
// RF-D02: Cadastrar uma nova tarefa (POST /api/tarefas)
public Tarefa criarTarefa(String descricao) throws IOException, InterruptedException {
Tarefa nova = new Tarefa(descricao);
String jsonBody = objectMapper.writeValueAsString(nova);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(5))
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 201 || response.statusCode() == 200) {
return objectMapper.readValue(response.body(), Tarefa.class);
} else {
throw new IOException("Falha ao criar tarefa. Código HTTP: " + response.statusCode());
}
}
// RF-D03: Alternar status de conclusão (PUT /api/tarefas/{id})
public Tarefa alternarStatus(Tarefa tarefa) throws IOException, InterruptedException {
tarefa.setConcluida(!tarefa.isConcluida());
String jsonBody = objectMapper.writeValueAsString(tarefa);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl + "/" + tarefa.getId()))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(5))
.PUT(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
return objectMapper.readValue(response.body(), Tarefa.class);
} else {
throw new IOException("Falha ao alternar status da tarefa #" + tarefa.getId() + ". Código: " + response.statusCode());
}
}
// RF-D04: Atualizar a descrição da tarefa (PUT /api/tarefas/{id})
public Tarefa atualizarDescricao(Long id, String novaDescricao, boolean concluida) throws IOException, InterruptedException {
Tarefa atualizada = new Tarefa(id, novaDescricao, concluida);
String jsonBody = objectMapper.writeValueAsString(atualizada);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl + "/" + id))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(5))
.PUT(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
return objectMapper.readValue(response.body(), Tarefa.class);
} else {
throw new IOException("Falha ao atualizar tarefa #" + id + ". Código: " + response.statusCode());
}
}
// RF-D05: Excluir uma tarefa por ID (DELETE /api/tarefas/{id})
public void excluirTarefa(Long id) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl + "/" + id))
.timeout(Duration.ofSeconds(5))
.DELETE()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 204 && response.statusCode() != 200) {
throw new IOException("Falha ao excluir tarefa #" + id + ". Código HTTP: " + response.statusCode());
}
}
}
🎯 Checkpoint de Verificação 3: Comunicação REST sem Bibliotecas Externas
- O que você acabou de implementar: Um cliente HTTP nativo em Java que se comunica com a mesma API consumida pelo Angular.
TarefaSwingFrame.java)Construímos a janela principal (JFrame) utilizando componentes clássicos do Swing com renderização customizada para badges de status e execução assíncrona (CompletableFuture + SwingUtilities.invokeLater):
src/main/java/br/com/curso/listadetarefas/desktop/TarefaSwingFrame.javapackage br.com.curso.listadetarefas.desktop;
import javax.swing.*;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Interface Gráfica Principal do Cliente Desktop desenvolvida em Java Swing puro.
* Conecta-se à API REST Spring Boot na porta 8080 e sincroniza operações CRUD.
*/
public class TarefaSwingFrame extends JFrame {
private final TarefaApiService apiService;
private final List<Tarefa> tarefasList = new ArrayList<>();
// Componentes de UI
private JTextField txtNovaTarefa;
private JButton btnAdicionar;
private JTable tabelaTarefas;
private DefaultTableModel tableModel;
private JLabel lblStatusConexao;
private JButton btnAlternar;
private JButton btnEditar;
private JButton btnExcluir;
private JButton btnAtualizar;
public TarefaSwingFrame() {
this.apiService = new TarefaApiService();
initComponents();
carregarTarefasAssincrono();
}
private void initComponents() {
setTitle("Lista de Tarefas — Cliente Desktop Swing");
setSize(750, 520);
setMinimumSize(new Dimension(650, 420));
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new BorderLayout(10, 10));
// 1. Cabeçalho (Topo)
JPanel painelTopo = new JPanel(new BorderLayout(5, 5));
painelTopo.setBorder(BorderFactory.createEmptyBorder(12, 16, 5, 16));
JLabel lblTitulo = new JLabel("📝 Minhas Tarefas (Desktop Swing)");
lblTitulo.setFont(new Font("Segoe UI", Font.BOLD, 18));
lblTitulo.setForeground(new Color(25, 42, 86));
lblStatusConexao = new JLabel("🔄 Conectando à API (http://localhost:8080)...");
lblStatusConexao.setFont(new Font("Segoe UI", Font.PLAIN, 12));
lblStatusConexao.setForeground(Color.GRAY);
painelTopo.add(lblTitulo, BorderLayout.NORTH);
painelTopo.add(lblStatusConexao, BorderLayout.SOUTH);
// 2. Formulário de Cadastro (Acima da Tabela)
JPanel painelForm = new JPanel(new BorderLayout(8, 8));
painelForm.setBorder(BorderFactory.createEmptyBorder(5, 16, 10, 16));
txtNovaTarefa = new JTextField();
txtNovaTarefa.setFont(new Font("Segoe UI", Font.PLAIN, 14));
txtNovaTarefa.setToolTipText("Digite o que você precisa fazer e pressione Enter...");
btnAdicionar = new JButton("➕ Adicionar");
btnAdicionar.setFont(new Font("Segoe UI", Font.BOLD, 13));
btnAdicionar.setBackground(new Color(46, 204, 113));
btnAdicionar.setForeground(Color.WHITE);
btnAdicionar.setFocusPainted(false);
btnAdicionar.setCursor(new Cursor(Cursor.HAND_CURSOR));
painelForm.add(new JLabel("Nova Tarefa:"), BorderLayout.WEST);
painelForm.add(txtNovaTarefa, BorderLayout.CENTER);
painelForm.add(btnAdicionar, BorderLayout.EAST);
JPanel containerNorte = new JPanel(new BorderLayout());
containerNorte.add(painelTopo, BorderLayout.NORTH);
containerNorte.add(painelForm, BorderLayout.SOUTH);
add(containerNorte, BorderLayout.NORTH);
// 3. Tabela Central com Lista de Tarefas
String[] colunas = {"ID", "Descrição da Tarefa", "Status"};
tableModel = new DefaultTableModel(colunas, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
tabelaTarefas = new JTable(tableModel);
tabelaTarefas.setFont(new Font("Segoe UI", Font.PLAIN, 14));
tabelaTarefas.setRowHeight(28);
tabelaTarefas.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
tabelaTarefas.getTableHeader().setFont(new Font("Segoe UI", Font.BOLD, 13));
tabelaTarefas.getColumnModel().getColumn(0).setPreferredWidth(60);
tabelaTarefas.getColumnModel().getColumn(0).setMaxWidth(80);
tabelaTarefas.getColumnModel().getColumn(1).setPreferredWidth(450);
tabelaTarefas.getColumnModel().getColumn(2).setPreferredWidth(120);
tabelaTarefas.getColumnModel().getColumn(2).setMaxWidth(140);
// Renderizador customizado para status
tabelaTarefas.getColumnModel().getColumn(2).setCellRenderer(new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
setHorizontalAlignment(JLabel.CENTER);
String status = value != null ? value.toString() : "";
if ("Concluída".equals(status)) {
setForeground(new Color(39, 174, 96));
setFont(getFont().deriveFont(Font.BOLD));
} else {
setForeground(new Color(230, 126, 34));
setFont(getFont().deriveFont(Font.PLAIN));
}
if (isSelected) {
setBackground(table.getSelectionBackground());
} else {
setBackground(row % 2 == 0 ? Color.WHITE : new Color(248, 249, 250));
}
return c;
}
});
JScrollPane scrollPane = new JScrollPane(tabelaTarefas);
scrollPane.setBorder(BorderFactory.createEmptyBorder(0, 16, 0, 16));
add(scrollPane, BorderLayout.CENTER);
// 4. Rodapé com Botões de Ação
JPanel painelRodape = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 12));
painelRodape.setBorder(BorderFactory.createEmptyBorder(5, 16, 10, 16));
btnAlternar = new JButton("✔️ Concluir / Reabrir");
btnAlternar.setFont(new Font("Segoe UI", Font.PLAIN, 13));
btnAlternar.setCursor(new Cursor(Cursor.HAND_CURSOR));
btnEditar = new JButton("✏️ Editar");
btnEditar.setFont(new Font("Segoe UI", Font.PLAIN, 13));
btnEditar.setCursor(new Cursor(Cursor.HAND_CURSOR));
btnExcluir = new JButton("🗑️ Excluir");
btnExcluir.setFont(new Font("Segoe UI", Font.PLAIN, 13));
btnExcluir.setForeground(new Color(192, 57, 43));
btnExcluir.setCursor(new Cursor(Cursor.HAND_CURSOR));
btnAtualizar = new JButton("🔄 Atualizar");
btnAtualizar.setFont(new Font("Segoe UI", Font.PLAIN, 13));
btnAtualizar.setCursor(new Cursor(Cursor.HAND_CURSOR));
painelRodape.add(btnAlternar);
painelRodape.add(btnEditar);
painelRodape.add(btnExcluir);
painelRodape.add(btnAtualizar);
add(painelRodape, BorderLayout.SOUTH);
// 5. Registro de Eventos
btnAdicionar.addActionListener(e -> acaoAdicionar());
txtNovaTarefa.addActionListener(e -> acaoAdicionar());
btnAlternar.addActionListener(e -> acaoAlternarStatus());
btnEditar.addActionListener(e -> acaoEditar());
btnExcluir.addActionListener(e -> acaoExcluir());
btnAtualizar.addActionListener(e -> carregarTarefasAssincrono());
tabelaTarefas.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
acaoEditar();
}
}
});
}
public void carregarTarefasAssincrono() {
lblStatusConexao.setText("🔄 Sincronizando com a API...");
lblStatusConexao.setForeground(Color.BLUE);
CompletableFuture.supplyAsync(() -> {
try {
return apiService.listarTarefas();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}).thenAccept(tarefas -> SwingUtilities.invokeLater(() -> {
tarefasList.clear();
tarefasList.addAll(tarefas);
tableModel.setRowCount(0);
for (Tarefa t : tarefas) {
tableModel.addRow(new Object[]{t.getId(), t.getDescricao(), t.isConcluida() ? "Concluída" : "Pendente"});
}
lblStatusConexao.setText("🟢 Conectado à API (http://localhost:8080) — " + tarefas.size() + " tarefas sincronizadas");
lblStatusConexao.setForeground(new Color(39, 174, 96));
})).exceptionally(ex -> {
SwingUtilities.invokeLater(() -> {
lblStatusConexao.setText("🔴 Falha ao conectar na API Spring Boot (Porta 8080)");
lblStatusConexao.setForeground(new Color(192, 57, 43));
});
return null;
});
}
private void acaoAdicionar() {
String descricao = txtNovaTarefa.getText().trim();
if (descricao.isEmpty()) {
JOptionPane.showMessageDialog(this, "Por favor, digite a descrição da tarefa!", "Aviso", JOptionPane.WARNING_MESSAGE);
return;
}
btnAdicionar.setEnabled(false);
CompletableFuture.runAsync(() -> {
try {
apiService.criarTarefa(descricao);
SwingUtilities.invokeLater(() -> {
txtNovaTarefa.setText("");
carregarTarefasAssincrono();
});
} catch (Exception ex) {
SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "Erro ao criar tarefa: " + ex.getMessage(), "Erro", JOptionPane.ERROR_MESSAGE));
} finally {
SwingUtilities.invokeLater(() -> btnAdicionar.setEnabled(true));
}
});
}
private void acaoAlternarStatus() {
int selectedRow = tabelaTarefas.getSelectedRow();
if (selectedRow < 0) {
JOptionPane.showMessageDialog(this, "Selecione uma tarefa na tabela para alterar o status!", "Aviso", JOptionPane.INFORMATION_MESSAGE);
return;
}
Tarefa tarefa = tarefasList.get(selectedRow);
CompletableFuture.runAsync(() -> {
try {
apiService.alternarStatus(tarefa);
SwingUtilities.invokeLater(this::carregarTarefasAssincrono);
} catch (Exception ex) {
SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "Erro ao alterar status: " + ex.getMessage(), "Erro", JOptionPane.ERROR_MESSAGE));
}
});
}
private void acaoEditar() {
int selectedRow = tabelaTarefas.getSelectedRow();
if (selectedRow < 0) {
JOptionPane.showMessageDialog(this, "Selecione uma tarefa na tabela para editar!", "Aviso", JOptionPane.INFORMATION_MESSAGE);
return;
}
Tarefa tarefa = tarefasList.get(selectedRow);
String novaDescricao = JOptionPane.showInputDialog(this, "Editar descrição da tarefa #" + tarefa.getId() + ":", tarefa.getDescricao());
if (novaDescricao != null && !novaDescricao.trim().isEmpty()) {
CompletableFuture.runAsync(() -> {
try {
apiService.atualizarDescricao(tarefa.getId(), novaDescricao.trim(), tarefa.isConcluida());
SwingUtilities.invokeLater(this::carregarTarefasAssincrono);
} catch (Exception ex) {
SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "Erro ao editar tarefa: " + ex.getMessage(), "Erro", JOptionPane.ERROR_MESSAGE));
}
});
}
}
private void acaoExcluir() {
int selectedRow = tabelaTarefas.getSelectedRow();
if (selectedRow < 0) {
JOptionPane.showMessageDialog(this, "Selecione uma tarefa na tabela para excluir!", "Aviso", JOptionPane.INFORMATION_MESSAGE);
return;
}
Tarefa tarefa = tarefasList.get(selectedRow);
int confirm = JOptionPane.showConfirmDialog(this, "Tem certeza que deseja excluir a tarefa #" + tarefa.getId() + " (" + tarefa.getDescricao() + ")?", "Confirmação", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
if (confirm == JOptionPane.YES_OPTION) {
CompletableFuture.runAsync(() -> {
try {
apiService.excluirTarefa(tarefa.getId());
SwingUtilities.invokeLater(this::carregarTarefasAssincrono);
} catch (Exception ex) {
SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "Erro ao excluir tarefa: " + ex.getMessage(), "Erro", JOptionPane.ERROR_MESSAGE));
}
});
}
}
}
MainSwingApp.java)Configuramos o Look and Feel nativo do sistema operacional e despachamos a inicialização para a Event Dispatch Thread:
src/main/java/br/com/curso/listadetarefas/desktop/MainSwingApp.javapackage br.com.curso.listadetarefas.desktop;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
/**
* Ponto de entrada (Main) do Cliente Desktop Java Swing.
* Configura o Look and Feel nativo do sistema operacional e inicializa a janela na Event Dispatch Thread (EDT).
*/
public class MainSwingApp {
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ignored) {
}
SwingUtilities.invokeLater(() -> {
TarefaSwingFrame frame = new TarefaSwingFrame();
frame.setVisible(true);
});
}
}
🎯 Checkpoint de Verificação 4: Construção do Pacote Executável
- Como validar agora: Na pasta
desktop-swing, execute:.\mvnw.cmd clean packageO Maven gerará o arquivo executável completo
target/desktop-swing-1.0-SNAPSHOT.jarcom status[INFO] BUILD SUCCESS.
Abra o terminal na pasta desktop-swing e execute o .jar gerado:
java -jar target/desktop-swing-1.0-SNAPSHOT.jar
A janela nativa do Swing abrirá imediatamente conectando-se à API!
Execute o script listadetarefas-painel.ps1:
.\listadetarefas-painel.ps1
No menu interativo:
1 para iniciar a API REST.3 para iniciar o Frontend Web Svelte na porta 5173.5 para abrir o Cliente Desktop Swing.http://localhost:5173 (Svelte Web da Parte 02) ou http://localhost:8080/tarefas (Thymeleaf da Parte 01) e recarregue a página. A tarefa estará lá!Concluída em verde!| Checkpoint | Ação / Cenário de Teste | Critério de Aceite | Status |
|---|---|---|---|
| CP-01 | Inicializar o app via java -jar com a API ativa |
Janela Swing carrega dados da API; barra de status exibe 🟢 Conectado à API. |
PASS ✅ |
| CP-02 | Digitar “Validar cliente Swing” e clicar em Adicionar | Requisição POST disparada; nova linha surge na tabela com ID atribuído pela API. |
PASS ✅ |
| CP-03 | Selecionar linha e clicar em “Concluir / Reabrir” | Status muda para “Concluída” em verde; API recebe PUT /api/tarefas/{id}. |
PASS ✅ |
| CP-04 | Dar duplo clique na descrição e alterar texto no diálogo | Texto da tarefa é atualizado in-place no servidor e na linha da JTable. |
PASS ✅ |
| CP-05 | Selecionar linha, clicar em “Excluir” e confirmar no diálogo | Requisição DELETE enviada; registro é removido do banco H2 e da tabela. |
PASS ✅ |
| Sintoma | Causa Mais Provável | Solução |
|---|---|---|
🔴 Falha ao conectar na API Spring Boot |
A API na porta 8080 está desligada. | Inicie a API na pasta canônica fullstack_tarefas_01_spring_api/api-spring. |
UnsupportedClassVersionError |
Versão do Java inferior à 17. | Verifique sua instalação com java --version e garanta JDK 17 ou superior. |
| A tabela não atualiza após mudanças na Web | O cliente Swing não possui websockets nesta etapa. | Clique no botão 🔄 Atualizar para buscar as alterações recentes da API. |
Parabéns! Você construiu com sucesso um cliente desktop nativo clássico que opera como um cidadão de primeira classe no ecossistema corporativo.
Agora, a sua jornada de engenharia de software ramifica-se em duas direções complementares:
Leve a mesmíssima API Spring Boot para o ambiente dos smartphones! Na Parte 04, você aprenderá a lidar com a virtualização do emulador Android (10.0.2.2:8080), layouts em XML e o componente de alta performance RecyclerView.
Quando desejar dominar a modernização da interface desktop corporativa, descubra na Parte 09 como substituir código Java imperativo por layouts declarativos em FXML, estilização com CSS, propriedades reativas (ObservableList) e arquitetura MVC rigorosa.