Monitoramento Inteligente de Chuvas no Rio de Janeiro
Aplicação Spring Boot que coleta, armazena e visualiza dados pluviométricos em tempo real das estações do Alerta Rio, cobrindo 33 estações espalhadas pelo município do Rio de Janeiro.
- Stack Tecnológica
- Arquitetura
- Design Patterns
- Virtual Threads
- Diagramas de Sequência
- Endpoints da API
- Como Executar
- Estrutura do Projeto
| Camada | Tecnologia |
|---|---|
| Linguagem | Java 25 |
| Framework | Spring Boot 4.0.3-SNAPSHOT |
| Web | Spring WebMVC + WebFlux |
| Persistência | Spring Data JPA + Hibernate Spatial |
| Banco | PostgreSQL 15 (via Docker) |
| Resiliência | Resilience4j (Circuit Breaker) |
| Scraping | Jsoup |
| Template | Thymeleaf + Chart.js |
| Infra | Docker Compose |
| Build | Maven |
O projeto segue a Arquitetura Hexagonal (Ports & Adapters), garantindo separação clara entre domínio, lógica de aplicação e infraestrutura.
com.hidrogeo/
├── domain/ # Núcleo do domínio (Records + Enum)
│ ├── Rainfall.java # Record com dados pluviométricos
│ ├── Incident.java # Record de incidentes urbanos
│ ├── Station.java # Enum com 33 estações do Rio
│ └── NeighborhoodRainfallStat.java # Record de estatísticas por bairro
│
├── application/ # Casos de uso e portas
│ ├── ports/
│ │ ├── in/ # Portas de entrada
│ │ │ ├── FetchExternalDataUseCase.java
│ │ │ └── GetWaterShortageStatsUseCase.java
│ │ └── out/ # Portas de saída
│ │ ├── RainfallPort.java
│ │ ├── SaveRainfallPort.java
│ │ ├── LoadRainfallPort.java
│ │ └── IncidentPort.java
│ ├── DataCollectionService.java # Orquestra coleta com Virtual Threads
│ └── DashboardService.java # Lógica de consulta e estatísticas
│
├── infrastructure/ # Adaptadores concretos
│ ├── adapters/
│ │ ├── in/
│ │ │ ├── web/DashboardController.java # Controller Thymeleaf + REST
│ │ │ └── scheduler/DataCollectionScheduler.java # Agendamento @Scheduled
│ │ └── out/
│ │ ├── external/
│ │ │ ├── AlertaRioAdapter.java # Scraping real do Alerta Rio
│ │ │ └── CorRioMockAdapter.java # Mock de incidentes
│ │ └── persistence/
│ │ ├── RainfallEntity.java # Entidade JPA
│ │ ├── RainfallRepository.java # Spring Data Repository
│ │ └── RainfallPersistenceAdapter.java # Adapter de persistência
│ └── config/
│ └── RainfallDataSeeder.java # Seed de 2 anos de dados
│
└── config/
├── AppConfig.java # Bean RestTemplate
└── ResilienceConfig.java # Configuração Circuit Breaker
O domínio define interfaces (portas) e a infraestrutura fornece as implementações (adaptadores), desacoplando completamente o core da aplicação das tecnologias externas.
- Portas de Entrada:
FetchExternalDataUseCase,GetWaterShortageStatsUseCase - Portas de Saída:
RainfallPort,SaveRainfallPort,LoadRainfallPort,IncidentPort - Adaptadores:
AlertaRioAdapter,RainfallPersistenceAdapter,CorRioMockAdapter
O AlertaRioAdapter usa @CircuitBreaker para proteger chamadas ao Alerta Rio. Quando o site está indisponível, o fallback gera dados mock automaticamente, garantindo que o dashboard sempre funcione.
As portas de saída (RainfallPort, IncidentPort) funcionam como contratos de estratégia - os adaptadores concretos podem ser trocados sem alterar a lógica de aplicação.
Cada adaptador converte dados entre formatos externos e os records do domínio (ex: RainfallEntity ↔ Rainfall no RainfallPersistenceAdapter).
O DataCollectionScheduler utiliza @Scheduled do Spring para disparar a coleta de dados a cada 15 segundos, desacoplado da lógica de negócio via FetchExternalDataUseCase.
O RainfallDataSeeder implementa CommandLineRunner para popular o banco com 2 anos de dados históricos simulados ao iniciar, com variâncias sazonais e geográficas realistas.
O projeto utiliza Virtual Threads (Project Loom) em dois níveis:
spring:
threads:
virtual:
enabled: true # Todas as requisições HTTP usam virtual threadsEm DataCollectionService.execute(), a coleta de dados das 33 estações é feita concorrentemente via Executors.newVirtualThreadPerTaskExecutor():
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (Station station : Station.values()) {
executor.submit(() -> {
rainfallPort.fetchRainfall(stationId).ifPresentOrElse(
r -> saveRainfallPort.save(r),
() -> logger.warn("Failed to fetch...")
);
});
}
} // Aguarda todas as threads completaremIsso permite que todas as 33 requisições de scraping rodem em paralelo sem consumir threads do sistema operacional.
sequenceDiagram
participant SCH as DataCollectionScheduler
participant DCS as DataCollectionService
participant VT as Virtual Thread Pool
participant ARA as AlertaRioAdapter
participant CB as Circuit Breaker
participant WEB as Alerta Rio Website
participant RPA as RainfallPersistenceAdapter
participant DB as PostgreSQL
SCH->>DCS: execute() [@Scheduled 15s]
loop Para cada estação (33x em paralelo)
DCS->>VT: submit(task)
VT->>ARA: fetchRainfall(stationId)
ARA->>CB: @CircuitBreaker check
alt Circuit Breaker CLOSED
CB->>WEB: HTTP GET (Jsoup scraping)
WEB-->>CB: HTML Response
CB-->>ARA: Parse tabela → Rainfall
else Circuit Breaker OPEN
CB->>ARA: fetchRainfallFallback()
ARA-->>ARA: Gerar dados mock
end
ARA-->>DCS: Optional<Rainfall>
DCS->>RPA: save(rainfall)
RPA->>DB: INSERT rainfall
end
DCS->>DCS: fetchIncidents() [mock]
sequenceDiagram
participant USR as Navegador
participant DC as DashboardController
participant TH as Thymeleaf
participant DS as DashboardService
participant RPA as RainfallPersistenceAdapter
participant DB as PostgreSQL
USR->>DC: GET /
DC->>TH: Renderizar index.html
TH-->>USR: HTML + Chart.js
USR->>DC: GET /api/stats (AJAX)
DC->>DS: getRainfallStatsSortedByShortage()
DS->>RPA: loadRecentRainfall()
RPA->>DB: SELECT DISTINCT ON (station_id) ...
DB-->>RPA: List<RainfallEntity>
RPA-->>DS: List<Rainfall>
DS-->>DC: Lista ordenada por volume 24h
DC-->>USR: JSON Response
USR->>DC: GET /api/history/neighborhood (AJAX)
DC->>DS: getHistoricalAverageByNeighborhood(2)
DS->>RPA: loadHistoricalRainfall(2)
RPA->>DB: SELECT ... WHERE timestamp BETWEEN ...
DB-->>RPA: List<RainfallEntity>
RPA-->>DS: List<NeighborhoodRainfallStat>
DC-->>USR: JSON Response
sequenceDiagram
participant APP as Spring Boot
participant SEED as RainfallDataSeeder
participant REPO as RainfallRepository
participant DB as PostgreSQL
APP->>APP: Inicialização
APP->>SEED: CommandLineRunner.run()
SEED->>REPO: deleteAll()
REPO->>DB: TRUNCATE rainfall
loop Para cada estação (33 estações × ~730 dias)
SEED->>SEED: Calcular multiplicador regional
SEED->>SEED: Gerar dados com sazonalidade
end
SEED->>REPO: saveAll(entities)
REPO->>DB: BATCH INSERT (~7000+ registros)
SEED-->>APP: Seeding concluído
| Método | Rota | Descrição |
|---|---|---|
| GET | / |
Dashboard HTML com gráficos Chart.js |
| GET | /api/stats |
Dados de chuva em tempo real (JSON) |
| GET | /api/history |
Dados históricos de chuva (JSON) |
| GET | /api/history/neighborhood |
Média de chuva por bairro - 2 anos (JSON) |
- Java 25+
- Maven 3.9+
- Docker e Docker Compose
docker compose up -d./mvnw spring-boot:runAbra o navegador em http://localhost:8080
O docker-compose.yml provisiona um PostgreSQL 15 Alpine com as configurações alinhadas ao application.yaml:
| Configuração | docker-compose | application.yaml |
|---|---|---|
| Database | POSTGRES_DB: hidrogeo |
url: ...localhost:5432/hidrogeo |
| User | POSTGRES_USER: postgres |
username: postgres |
| Password | POSTGRES_PASSWORD: password |
password: password |
| Port | 5432:5432 |
localhost:5432 |
Volume persistente postgres_data garante que os dados sobrevivem a reinicializações do container.
O projeto inclui um teste de contexto (HidroGeoApplicationTests) que valida que o contexto Spring carrega sem erros. Os starters de teste incluídos são:
spring-boot-starter-actuator-testspring-boot-starter-data-jpa-testspring-boot-starter-webflux-testspring-boot-starter-webmvc-test
Projeto acadêmico / laboratório de estudo.