Rename project and add interactive Thymeleaf showcase

This commit is contained in:
2026-08-19 21:46:29 +08:00
parent 10addc4d52
commit 11894f5998
41 changed files with 647 additions and 748 deletions
+45 -140
View File
@@ -1,174 +1,79 @@
# Spring Boot JSONPlaceholder Showcase
# Spring Boot Demo
A compact, production-minded REST API that turns the original JSONPlaceholder experiment into a public portfolio project. It demonstrates how I structure a modern Spring Boot application: clean HTTP contracts, secure write operations, JPA persistence, cache-aware reads, external API integration, tested behaviour, and environment-driven configuration.
An interactive portfolio application demonstrating how I build a modern Spring Boot system from browser page to database. It replaces the original third-party API experiment with a self-contained demo: a Thymeleaf landing page, a live feature tour, secured REST APIs, JPA persistence, cache metrics, validation, error handling, and tested behaviour.
The application is intentionally a runnable API rather than a full product. It is small enough to review quickly, but its patterns scale to admin portals, booking systems, internal tools, marketplace back ends, and integration services.
## Live pages
## What this demonstrates
| Capability | Implementation |
| URL | Purpose |
| --- | --- |
| Modern Java and dependencies | Java 21, Spring Boot 3.5.16, Maven Wrapper 3.9.11, dependency versions managed by Spring Boot's BOM |
| REST API design | `POST`, `GET`, `PUT`, and `DELETE` endpoints with a stable page envelope and proper `201`, `204`, `400`, `401`, `404`, `409`, and `503` responses |
| Spring Data JPA | UUID primary keys, derived search query, pagination, audit timestamps, transaction boundaries, and optimistic locking |
| Caching | Bounded Caffeine cache for individual read operations; update, delete, and import paths keep it coherent |
| Security | Stateless HTTP Basic authentication for writes, BCrypt password hashing, least-privilege route rules |
| External integration | Spring's `RestClient`, explicit timeout, separate remote DTO, failure translation, and idempotent JSONPlaceholder import |
| Configuration management | Validated typed configuration, local H2 defaults, a PostgreSQL production profile, environment-variable overrides, no machine paths, no committed production credentials |
| Reliability | RFC 9457-style `ProblemDetail` errors, input validation, health endpoint, and integration tests |
| `/` | Thymeleaf landing page based on my Spring portfolio content, with developer profile and contact details. |
| `/showcase` | Interactive jQuery AJAX tour. It calls the running controllers, shows JPA query results, creates a database record, reads a post twice, and displays live cache statistics. |
| `/api/posts` | Public paginated/searchable REST read API; `POST`, `PUT`, and `DELETE` require the `EDITOR` role. |
| `/api/showcase/cache` | Read-only Caffeine cache statistics for the feature tour. |
| `/actuator/health` | Public health check. |
## Architecture
## Run it
```text
HTTP client
├── PostController ── request/response DTOs
│ │
│ └── PostService ── transactions, cache policy, import workflow
│ ├── PostRepository ── Spring Data JPA ── H2 / production database
│ └── JsonPlaceholderClient ── RestClient ── JSONPlaceholder
└── ApiExceptionHandler ── consistent problem responses
```
The controller does not contain persistence or business logic. The service owns the transaction and cache rules, the repository owns database access, and the integration client owns the remote contract. This makes each responsibility straightforward to test and replace.
## Run locally
Prerequisites: JDK 21+ and an internet connection the first time Maven downloads dependencies.
Requires JDK 21+.
```powershell
cd D:\app\springboot-jsonplaceholder-demo
./mvnw.cmd spring-boot:run
```
The API starts on `http://localhost:8080`. Check its health at `GET /actuator/health`.
Open [http://localhost:8080](http://localhost:8080). The local profile uses an in-memory H2 database and automatically loads three sample posts for the feature tour.
The checked-in account is deliberately a local demo account:
The local editor account is intentionally a non-secret demo account:
```text
username: demo-editor
password: changeit
```
It is not a secret and must be replaced outside local development. Use environment variables instead of editing configuration or committing credentials:
The showcase form never pre-fills or stores credentials; it sends them only to the same local application for the authenticated request. Override them through `APP_EDITOR_USERNAME` and `APP_EDITOR_PASSWORD`.
```powershell
$env:APP_EDITOR_USERNAME = "your-editor"
$env:APP_EDITOR_PASSWORD = "use-a-real-secret-manager-in-production"
./mvnw.cmd spring-boot:run
```
## What the feature tour proves
For a deployed service, the `prod` profile switches to PostgreSQL and requires `APP_DB_URL`, `APP_DB_USERNAME`, `APP_DB_PASSWORD`, `APP_EDITOR_USERNAME`, and `APP_EDITOR_PASSWORD`. It has no secret defaults:
1. **Thymeleaf & MVC**`HomeController` renders `/` and `/showcase`; the index uses a server-side model for contact values.
2. **jQuery AJAX & REST controllers** — the browser calls `GET /api/posts`, `POST /api/posts`, `GET /api/posts/{id}`, health, and cache-stat endpoints. Responses and errors are displayed directly in the page.
3. **Spring Data JPA**`PostRepository` provides pagination and case-insensitive title search; `PostService` owns read/write transaction boundaries.
4. **Caching** — individual post reads use bounded Caffeine caching. The “Read twice” button produces real cache activity, then requests the cache-stat endpoint.
5. **Security** — public portfolio pages and read APIs are open; data mutations require the `EDITOR` role with BCrypt-backed, stateless HTTP Basic authentication.
6. **Validation & error handling** — request records validate input at the boundary and `ApiExceptionHandler` returns consistent RFC-style problem documents.
7. **Safe concurrent updates** — the JPA `@Version` field makes an outdated update return a conflict instead of silently overwriting a newer change.
8. **Configuration management** — typed cache and security settings live in application configuration. The `prod` profile uses PostgreSQL and requires database/editor environment variables with no committed production secrets.
```powershell
$env:SPRING_PROFILES_ACTIVE = "prod"
$env:APP_DB_URL = "jdbc:postgresql://host:5432/postshowcase"
$env:APP_DB_USERNAME = "application_user"
$env:APP_DB_PASSWORD = "store-this-outside-git"
./mvnw.cmd spring-boot:run
```
For a real deployment, run schema migrations with Flyway or Liquibase and supply all values from the hosting platform's secret store.
## API quick start
Public read endpoints require no login.
```powershell
curl http://localhost:8080/api/posts
curl "http://localhost:8080/api/posts?title=spring&page=0&size=10"
```
Create, update, delete, and import operations require the editor account.
```powershell
curl -u demo-editor:changeit -X POST http://localhost:8080/api/posts `
-H "Content-Type: application/json" `
-d '{"authorId":1,"title":"Spring Boot showcase","body":"A practical API example."}'
curl -u demo-editor:changeit -X POST http://localhost:8080/api/posts/import/jsonplaceholder
```
An update includes the resource's `version`. This is a deliberate optimistic-locking contract: if another editor saves first, the API returns `409 POST_VERSION_CONFLICT` instead of overwriting their work.
```json
{
"title": "Revised title",
"body": "Revised body",
"version": 0
}
```
The JSONPlaceholder import limit, remote base URL, and timeout are application properties. Imported source IDs are recorded, so repeating the import safely skips records already imported.
## Test and build
## Build and test
```powershell
./mvnw.cmd test
./mvnw.cmd package
```
The integration test verifies the public/private route split, authenticated creation, validation error shape, title search, and duplicate-safe import. It substitutes the remote client, so the test suite does not rely on a live third-party service.
The integration test verifies the rendered Thymeleaf pages, unauthenticated versus editor-only API access, validation responses, search, and cache metrics.
## Learning journal
## Project layout
### 1. Prefer a standalone executable API over a legacy WAR
```text
com.hoelee.demo
├── config typed cache and security configuration
├── post REST contract, JPA entity, repository, and service layer
├── support consistent API error mapping
└── web Thymeleaf pages, cache metrics, and local demo data
```
The previous project used a Spring Boot 2.3 WAR with JSP/Thymeleaf remnants and Java 8. This rewrite uses a runnable JAR, Java 21, Spring Boot 3.5.16, and the Maven Wrapper. This removes servlet-container deployment assumptions and keeps the repository focused on back-end skills.
Each Java file has a compact learning note in the author/version comment format used by the larger reference application. The source uses 2024 timestamps in the requested evening window; the Git commits themselves retain their real creation times.
### 2. Keep API, domain, persistence, and remote models separate
## Production profile
`CreatePostRequest`, `UpdatePostRequest`, and `PostResponse` describe the public API. `Post` describes a database row. `JsonPlaceholderPost` describes a third-party response. Those models happen to look similar today, but they will evolve for different reasons; separating them prevents accidental breaking changes.
`SPRING_PROFILES_ACTIVE=prod` selects PostgreSQL and requires:
### 3. Validate at the edge and transact in the service
```text
APP_DB_URL
APP_DB_USERNAME
APP_DB_PASSWORD
APP_EDITOR_USERNAME
APP_EDITOR_PASSWORD
```
Jakarta Validation rejects bad JSON before business work starts. `PostService` then provides read-only and write transaction boundaries. The entity is intentionally free of controller annotations, which makes it reusable outside HTTP.
### 4. Cache only a narrow, safe read path
Only `GET /api/posts/{id}` is cached. Search results are dynamic and therefore intentionally not cached. Write methods update or evict the cached item, and imports clear the small cache. The Caffeine cache is size-bounded and expires after ten minutes, avoiding unbounded memory growth and stale values that linger forever.
### 5. Secure mutation, not a toy login screen
Anyone may read sample posts. Every operation that changes data requires the `EDITOR` role. The application uses stateless HTTP Basic authentication because it is easy to inspect in a small API; a browser or mobile application would normally sit behind OAuth2/OIDC or a gateway. Password configuration comes from environment variables and is BCrypt-hashed in memory.
### 6. Make integration failures part of the contract
The remote API is called through Spring `RestClient` with a configurable timeout. A client error becomes an application exception and then a `503` problem response; it never becomes an unstructured stack trace. The import has a source identifier and skips known records, making retries safe.
### 7. Use actionable errors and tests
The exception advice returns a predictable problem document with a machine-readable code and field-level validation errors. The integration tests execute real MVC, security, JPA, cache, and transaction wiring. Only the outbound HTTP boundary is mocked, keeping tests deterministic.
## File-by-file learning notes
Every Java source file carries a dated header and closing marker in the style of the larger reference application. The source notes are intentionally concise; this table is the fuller reading guide.
| File | Why it exists / learning focus |
| --- | --- |
| `ShowcaseApplication` | Enables bootstrapping, configuration-property discovery, and JPA auditing in one explicit place. |
| `config/ApiProperties` | Binds and validates remote API settings as typed values. |
| `config/SecurityProperties` | Keeps login values external to source code. |
| `config/CacheConfig` | Defines a bounded, expiring Caffeine cache. |
| `config/RestClientConfig` | Names and configures the remote HTTP client with connection and read timeouts. |
| `config/SecurityConfig` | Defines stateless route authorization and BCrypt-backed editor credentials. |
| `integration/JsonPlaceholderPost` | Isolates the third-party JSON contract. |
| `integration/JsonPlaceholderClient` | Converts remote HTTP calls into a small application-facing API. |
| `post/Post` | Maps the persistence model with UUID, audit fields, source ID, and optimistic version. |
| `post/PostRepository` | Shows pagination and case-insensitive search without handwritten SQL. |
| `post/CreatePostRequest` | Defines and validates the create contract. |
| `post/UpdatePostRequest` | Defines the update contract and requires the expected version. |
| `post/PostResponse` | Shields API consumers from JPA implementation details. |
| `post/PageResponse` | Keeps paginated JSON stable even if framework serialization changes. |
| `post/ImportResult` | Reports an import outcome compactly. |
| `post/PostService` | Centralizes transactions, cache coherence, conflict detection, and idempotent import rules. |
| `post/PostController` | Translates HTTP verbs, status codes, URI creation, and pagination into service calls. |
| `support/PostNotFoundException` | Represents a missing domain resource. |
| `support/PostVersionConflictException` | Represents a safe concurrent-update failure. |
| `support/UpstreamServiceException` | Prevents third-party errors leaking through the API. |
| `support/ApiExceptionHandler` | Turns expected failures into a consistent public error format. |
| `test/PostApiIntegrationTest` | Tests behaviour across the actual Spring web stack without a live remote dependency. |
## What I can build from this foundation
This structure is suitable for secure CRUD APIs, internal dashboards, workflow back ends, integrations with external SaaS APIs, catalogue or content services, and the server side of web or mobile applications. Depending on the product, I can extend it with PostgreSQL/MySQL, schema migrations, OAuth2/OIDC, role and permission models, file uploads, scheduled work, queue consumers, email, metrics/tracing, Docker, CI, and cloud deployment configuration.
For production, use a managed secret store and add Flyway or Liquibase migrations before setting `ddl-auto` to `validate`.