co-eiv-devsecops/linker1
GitHub: co-eiv-devsecops/linker1
一个基于 Java/Javalin 的简易 URL 缩短服务,兼具完整的 CI/CD 流水线与云原生可观测性实践。
Stars: 0 | Forks: 0
[](https://github.com/co-eiv-devsecops/linker1/actions/workflows/ci.yml)
[](https://github.com/co-eiv-devsecops/linker1/actions/workflows/pipeline.yml)
[](https://github.com/co-eiv-devsecops/linker1/actions/workflows/release.yml)
# Linker1
一个简单的 URL 缩短工具,使用 **Java** 和 **Javalin** 构建,搭配 SQLite 数据库和静态前端。
## 它是什么?
Linker1 是一个 Web 应用程序,它可以让你:
- 输入一个长 URL
- 生成一个短码(8 个字符),或者设置一个**自定义别名**
- 当你访问该短码或别名时自动重定向
## API
- `POST /link` — 创建一个短链接。JSON body:`{"url": "https://..."}`。如果该 URL 已经存在,返回 `200` 以及相同的短码;如果是新 URL,返回 `201`。
- 自定义别名(可选):`{"url": "https://...", "alias": "my-alias"}`。别名规则:只能包含字母、数字、连字符(`-`)和下划线(`_`),长度在 1 到 64 个字符之间,不能包含空格。如果该别名已被其他 URL 占用,返回 `409`;如果别名无效,返回 `400`。
- `GET /{id}` — 重定向(`301`)到与短码或别名关联的 URL。如果不存在,返回 `404`。
- `HEAD /{id}` — 解析短链接。如果找到,返回 `200` 以及响应 body 中的真实 URL(不进行重定向);如果未找到,返回 `404`。
- `DELETE /{id}` — 删除短链接。成功返回 `204`,如果短码或别名不存在,返回 `404`。
## 环境要求
- **Java 21**
- **Maven 3.7+**
- **Git**
## 在虚拟机(VM)上安装和运行
### 1. 克隆仓库
```
git clone
cd linker1
```
### 2. 构建项目
```
mvn clean package
```
### 3. 运行应用程序
```
java -jar target/linker1-1.0-jar-with-dependencies.jar
```
应用程序将通过 `http://localhost:8080` 访问
### 4. 部署
我们创建了一个脚本以简化部署过程;你只需在虚拟机上克隆仓库并运行:
```
bash deploy.sh
```
此脚本会:
- 从仓库拉取更改
- 安装依赖项(Java, Maven, Nginx)
- 构建项目
- 配置 systemd 服务
- 启动 Nginx 作为反向代理
- 在 8080 端口暴露应用程序
### 5. 环境对等 (IaC)
除了使用 `deploy.sh` 进行手动部署外,项目在 `infra/` 目录下还包含了使用 Terraform 的基础设施即代码,允许你以可复现的方式创建一个包含运行 Linker1 所需一切的新虚拟机。
**它的作用:** 在 OCI 上创建一个计算实例(与团队位于相同的子网和区间),并通过 cloud-init 自动配置:安装 Java 21、Maven、Nginx,克隆仓库、构建项目并启动服务——全程无需手动操作。
**要求:** Terraform >= 1.5,拥有 OCI 的访问权限(通过 OCI Cloud Shell,它自带身份验证)。
**使用方法:**
```
cd infra
cp terraform.tfvars.example terraform.tfvars
# 使用你的值填充 terraform.tfvars (compartment_id, subnet_id, image_id, ssh_public_key)
terraform init
terraform plan
terraform apply
```
**销毁环境:**
```
terraform destroy
```
### 验证是否正常运行
你应该能在以下 URL 看到静态页面:
```
https://1.n-la-c.app/
```
就像这样:

## 流水线与质量
CI 流水线(`.github/workflows/ci.yml`)会在每次向 `main`/`DEV` 分支 `push` 以及每次发起 `pull_request` 时运行,包含以下作业:
- **Build**: 编译代码(`mvn compile`)。
- **Tests**: 运行单元测试和集成测试(`mvn test`),并使用 JaCoCo 强制执行覆盖率阈值(`mvn verify`)。
- **Package**: 生成可执行的 jar(`target/linker1-1.0-jar-with-dependencies.jar`)以及启动脚本(`scripts/linker1`)。
- **Summary**: 确认打包的构建产物存在且可执行。
- **Smoke Test**: 下载打包好的 jar,启动它,并进行主路由(静态资源、创建链接、别名、重定向、404)的实时测试。
- **API Tests (Live)**: 仅在向 `main`/`DEV` 分支 `push` 时运行,使用 [Newman](https://github.com/postmanlabs/newman) 对已部署的实例(`https://1.n-la-c.app/`)运行 Postman 集合。在 pull request 上不运行,以免影响共享的生产环境。
- **Publish GitHub Package**: 在向 `main`/`DEV` 分支 `push` 时,将构建好的 jar 发布到 **GitHub Packages**(见下文)。
`main` 和 `DEV` 分支均受保护:它们要求至少获得一次批准,并且必须通过 Build/Tests/Package/Summary/Smoke Test 检查后才能允许合并。有关完整的配置和基本原理,请参阅 [`docs/BRANCH_PROTECTION.md`](docs/BRANCH_PROTECTION.md)。
### GitHub Packages
每次向 `main`/`DEV` 分支 push 时,都会通过 `pom.xml` 中的 `distributionManagement` 配置,将构建好的 jar 作为 Maven 包发布到 **GitHub Packages**(`https://maven.pkg.github.com/co-eiv-devsecops/linker1`)。每次 CI 发布的版本都会获得一个唯一的、无冲突的标识符(`-ci...`),因此重新运行 CI(包括手动重新运行同一作业)绝不会与之前发布的包发生冲突。这为团队提供了一种独立于 GitHub Releases 的、带版本号的、可下载的构建产物历史记录。
### 持续部署
每次向 `main` 分支的 `push` 还会触发 `.github/workflows/pipeline.yml`,它会构建 jar,通过 OCI Bastion 将其部署到生产 VM(该 VM 没有公网 IP),验证服务是否响应,并在出现任何失败时自动回滚到之前稳定的 tag。完整细节(作业、所需密钥、堡垒机机制)请见 [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md);回滚策略请见 [`docs/ROLLBACK_STRATEGY.md`](docs/ROLLBACK_STRATEGY.md)。
## 发布 (GitHub Release)
项目包含一个位于 `.github/workflows/release.yml` 的发布工作流,它会自动创建 **GitHub Release** 并附上可供下载的构建产物。
### 何时运行?
- 自动:当 push 一个格式为 `vMAJOR.MINOR.PATCH` 的 tag 时(例如 `v1.2.3`)。
- 手动:从 **Actions > Release > Run workflow** 运行,并指定一个符合该格式的现有 tag。
### 它会验证和发布什么?
在发布之前,工作流会运行:
- `mvn clean verify`(构建、测试和覆盖率规则)
- `mvn package -DskipTests`
然后它会创建/更新 Release 并附上:
- `linker1-1.0-jar-with-dependencies.jar`
- `linker1`(Linux 启动脚本)
### 推荐的版本控制流程
1. 创建一个语义化 tag:
git tag v1.2.3
git push origin v1.2.3
2. 等待 **Release** 工作流执行完毕。
3. 在 **GitHub > Releases** 中验证:
- `Linker1 v1.2.3` 版本已存在
- 两个构建产物均已附上
- 自动生成的发布说明已生成(并且可以选择进行编辑)
### 功能开关
前端有两个版本,`public/v1/`(默认)和 `public/v2/`(重新设计主题的版本),在请求时由 **LaunchDarkly** 功能开关(`new-ui`)决定提供哪一个版本,而不是通过构建时的开关或硬编码的环境变量来决定。`src/linker/config/FeatureFlags.java` 将一个真实的 `LDClient` 封在一个小型接口(`isNewUiEnabled()`)中;`Main.java` 在启动时构建一次 LaunchDarkly 客户端(读取 `LD_SDK_KEY`,如果缺失则拒绝启动),并通过其构造函数将 `FeatureFlags` 注入到 `StaticRoutes` 中,随后由 `StaticRoutes` 在每次请求时选择 `v1` 或 `v2`。这让团队能够将新 UI 作为代码发布,然后从 LaunchDarkly 控制面板逐步为真实用户开启——无需部署——并在确认新路径健康后淘汰旧的(“糟糕的”)代码路径。完整设计请参阅 [`docs/FEATURE_FLAGS.md`](docs/FEATURE_FLAGS.md)。
### 测试覆盖率
该项目使用 [JaCoCo](https://www.jacoco.org/jacoco/) 来测量代码覆盖率。目标是 100% 的行覆盖率(排除 `Main.class`,这个入口点会打开真实的数据库连接并启动真实的服务器——通常在单元覆盖率指标中将其排除在外)。报告通过 `mvn verify` 生成在 `target/site/jacoco/` 目录下。
### 使用 Postman/Newman 进行 API 测试
`postman/linker1.postman_collection.json` 集合涵盖了:静态路由、链接创建(有效情况、重复、无效 URL)、别名(有效情况、已占用别名、无效别名)以及重定向(按 id、按别名、404)。要在本地运行它:
```
npx newman run postman/linker1.postman_collection.json --env-var "baseUrl=http://localhost:8080"
```
## 可观测性
Linker1 集成了 [OpenTelemetry Java SDK](https://opentelemetry.io/docs/languages/java/):日志(通过 SLF4J/Logback,桥接到 OpenTelemetry)、指标(2 个 counters、2 个 gauges、2 个 histograms,涵盖 RED 方法及系统级 gauges)以及 traces(2 个 traces,每个包含一个父 span 和一个子 span),分布在链接创建和解析的周围。所有这些都是手动/编程式埋点——在 `src/` 中显式调用,而不是自动埋点的 javaagent——围绕一个小型可重用库(`linker.telemetry`)构建,因此代码库的其余部分仅通过少数几个窄范围的、构造函数注入的类与其交互,而不是直接调用原始的 OpenTelemetry API。
- **详细级别**:由 `LOG_LEVEL` 环境变量控制,在进程启动时读取——同一个可部署的 jar 可以在任何详细级别下运行,而无需重新构建。请参阅 [`docs/LOGGING.md`](docs/LOGGING.md)。
- **指标、traces 和埋点库的设计**:请参阅 [`docs/INSTRUMENTATION.md`](docs/INSTRUMENTATION.md)。
- **导出到后端(例如 Grafana Cloud)**:设置 `OTEL_EXPORTER_OTLP_ENDPOINT`(以及用于身份验证的 `OTEL_EXPORTER_OTLP_HEADERS`)——标准的 OpenTelemetry 环境变量,无需自定义配置机制。如果未设置,应用将正常运行,OTLP 导出只会在后台静默失败,而不是拒绝启动。
### 健康检查
`GET /healthz` 针对配置好的 MySQL 连接(通过 `MYSQL_HOST`/`MYSQL_DATABASE`/`MYSQL_USER`/`MYSQL_PWD` 配置)运行 `SELECT 1`,并返回 `200 OK` 或 `503 Unhealthy: `,封装在其独立的 `mysql.healthcheck` 服务器类型 span 中。这与 Linker1 实际的数据存储(SQLite)是分开的——它的存在纯粹是为了让外部监控(例如 Grafana Cloud Synthetic Monitoring)能够轮询单个端点,以验证已部署实例的 MySQL 依赖项是否可访问。请参阅 [`docs/HEALTHCHECK.md`](docs/HEALTHCHECK.md)。
### 监控仪表板
一个涵盖 RED 方法 HTTP 指标、JVM/进程 gauges、`/healthz` 的上下运行状态以及链接数量的 Grafana 仪表板已提交至 [`docs/grafana/linker1-dashboard.json`](docs/grafana/linker1-dashboard.json)。`pipeline.yml` 的 `deploy-prod` 作业也会在每次部署后运行 [`scripts/check-grafana-metrics.sh`](scripts/check-grafana-metrics.sh),以确认新实例确实在向 Grafana Cloud 发送遥测数据,而不仅仅是在本地响应 `/healthz`。有关如何导航仪表板、逐面板的事件指南以及自动化检查所需(但尚未配置)的密钥,请参阅 [`docs/MONITORING.md`](docs/MONITORING.md)。
## 使用 Dev Container 运行 (Visual Studio Code)
作为本地安装的替代方案,该项目可以使用 **Dev Container** 运行。此选项提供了一个可复现的开发环境,避免了在你的机器上手动安装项目依赖项。
### Dev Container 要求
- Docker Desktop (Windows/macOS) 或 Docker Engine (Linux)。
- Visual Studio Code。
- Microsoft 的 **Dev Containers** 扩展。
### 1. 在 Dev Container 中打开项目
在 Visual Studio Code 中克隆并打开仓库后:
1. 下 `Ctrl + Shift + P`。
2. 运行命令:
```
Dev Containers: Reopen in Container
```
如果是第一次以这种方式打开项目,Visual Studio Code 将自动构建容器镜像并配置开发环境。
### 2. 等待环境配置完成
容器构建完成后,Visual Studio Code 将在配置好的开发环境中重新打开该项目。
项目所需的所有依赖项将自动可用。
### 3. 构建项目
在 Visual Studio Code 的集成终端中,运行:
```
mvn clean package
```
### 4. 运行应用程序
在同一个终端中,运行:
```
java -jar target/linker1-1.0-jar-with-dependencies.jar
```
### 5. 测试应用程序
打开浏览器并访问:
```
http://localhost:8080
```
### 6. 验证其是否有效
1. 输入完整的 URL(例如:`https://www.google.com`)。
2. 生成短链接。
3. 复制生成的代码。
4. 访问:
以验证重定向是否正常工作。
### 重建 Dev Container
如果对 `Dockerfile` 或 `devcontainer.json` 进行了更改,请通过运行以下命令从命令面板(`Ctrl + Shift + P`)重建环境:
```
Dev Containers: Rebuild and Reopen in Container
```
## 项目结构
```
linker1/
├── .github/ # CI workflows and community health files
│ ├── workflows/
│ │ ├── ci.yml # Pipeline: build, tests, package, summary, smoke test, API tests, GitHub Package publish
│ │ ├── pipeline.yml # Continuous deployment: build, deploy-prod, validate-prod, rollback
│ │ └── release.yml # GitHub Release on vMAJOR.MINOR.PATCH tags
│ ├── CONTRIBUTING.md # Collaboration standards (branching, TDD, PRs)
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── ISSUE_TEMPLATE/ # Bug report / feature request templates
│ └── CODEOWNERS
├── docs/ # Deployment, rollback, branch protection, and observability docs
│ ├── DEPLOYMENT.md
│ ├── ROLLBACK_STRATEGY.md
│ ├── BRANCH_PROTECTION.md
│ ├── FEATURE_FLAGS.md
│ ├── LOGGING.md # LOG_LEVEL configuration
│ ├── INSTRUMENTATION.md # OpenTelemetry metrics/traces/logs design
│ ├── HEALTHCHECK.md # GET /healthz: MySQL SELECT 1, span, Grafana Synthetic Monitoring
│ ├── MONITORING.md # Grafana dashboard guide + post-deploy telemetry check
│ └── grafana/
│ └── linker1-dashboard.json # Exported/importable Grafana dashboard
├── infra/ # Infrastructure as code (Terraform)
│ ├── main.tf
│ ├── variables.tf
│ ├── provider.tf
│ ├── outputs.tf
│ ├── cloud-init.yaml
│ └── terraform.tfvars.example
├── resources/
│ └── logback.xml # Logging config: console + OTel appenders, LOG_LEVEL-driven verbosity
├── src/
│ ├── Main.java # Bootstrap: DB, telemetry SDK, LaunchDarkly client, Javalin server, route registration
│ └── linker/ # Business logic and data access
│ ├── Link.java
│ ├── LinkService.java # Validation, id generation, alias
│ ├── LinkRepository.java # JDBC access to SQLite
│ ├── AliasConflictException.java
│ ├── config/
│ │ └── FeatureFlags.java # LaunchDarkly wrapper, injected via constructor
│ ├── telemetry/ # OpenTelemetry wrappers, injected via constructor
│ │ ├── Telemetry.java # Builds the OpenTelemetry SDK from OTEL_* env vars
│ │ ├── RequestMetrics.java # RED-method HTTP counters/histogram
│ │ ├── LinkSpans.java # Traces for link create/resolve + DB duration histogram
│ │ └── SystemMetrics.java # Link-count and JVM heap gauges
│ ├── health/ # MySQL healthcheck, injected via constructor
│ │ ├── HealthCheck.java # SELECT 1 against MySQL, wrapped in a SpanKind.SERVER span
│ │ └── HealthRoutes.java # GET /healthz -> 200/503
│ └── routes/ # HTTP handlers (Javalin), constructor-injected dependencies
│ ├── LinkRoutes.java
│ └── StaticRoutes.java
├── test/ # Unit and integration tests (JUnit 5)
├── postman/ # Postman collection for API tests (Newman)
├── public/
│ ├── v1/ # Default frontend
│ │ ├── index.html
│ │ └── styles.css
│ └── v2/ # Feature-flagged redesign
│ ├── index-v2.html
│ └── styles2.css
├── scripts/
│ ├── linker1 # Executable launcher script (packaged alongside the jar)
│ ├── rollback.sh # Rolls the VM back to a previous stable tag
│ └── check-grafana-metrics.sh # Post-deploy check: confirms telemetry reached Grafana Cloud
├── pom.xml # Maven configuration (JaCoCo, GitHub Packages, LaunchDarkly SDK, OpenTelemetry)
├── deploy.sh # Deployment script (OCI)
└── README.md # This file
```
## 团队
- ANDERSSON DAVID SÁNCHEZ MÉNDEZ
- ANDERSON FABIAN GARCIA NIETO
- DANIEL PATIÑO MEJIA
- ESTEBAN AGUILERA CONTRERAS
- JUAN JOSE DIAZ GOMEZ
标签:域名枚举, 用户代理, 请求拦截