從環(huán)境變量到配置中心帶你掌握Python多環(huán)境配置
引言:一次慘痛的線上事故
凌晨?jī)牲c(diǎn),手機(jī)突然震動(dòng)。
“生產(chǎn)環(huán)境數(shù)據(jù)庫(kù)連接失敗,核心業(yè)務(wù)全部中斷!”
我從睡夢(mèng)中驚醒,打開電腦,定睛一看——有人提交了一行代碼,把 config.py 里的數(shù)據(jù)庫(kù)地址從生產(chǎn)環(huán)境硬編碼成了開發(fā)環(huán)境的地址。這一個(gè)低級(jí)錯(cuò)誤,導(dǎo)致整個(gè)系統(tǒng)停擺了四十分鐘。
這是我職業(yè)生涯中刻骨銘心的一次教訓(xùn)。從那以后,我對(duì) Python 配置管理的重視程度,不亞于對(duì)核心業(yè)務(wù)邏輯的重視。
配置管理,是大型 Python 項(xiàng)目中最容易被忽視、卻最容易引發(fā)生產(chǎn)事故的環(huán)節(jié)之一。
今天,我將結(jié)合多年 Python 實(shí)戰(zhàn)經(jīng)驗(yàn),系統(tǒng)講解配置管理的三個(gè)層次:環(huán)境變量、配置文件、配置中心,以及如何在不同場(chǎng)景下選擇合適的方案,構(gòu)建穩(wěn)定、安全、可維護(hù)的配置體系。
一、配置管理的核心矛盾
在深入技術(shù)細(xì)節(jié)之前,先來理解配置管理要解決的根本問題。
1.1 三個(gè)環(huán)境,三張面孔
任何一個(gè)稍具規(guī)模的 Python 項(xiàng)目,都會(huì)面對(duì)至少三套環(huán)境:
- 開發(fā)環(huán)境(Development):本地調(diào)試,連接本地?cái)?shù)據(jù)庫(kù),開啟詳細(xì)日志
- 測(cè)試環(huán)境(Staging):接近生產(chǎn),用于集成測(cè)試和驗(yàn)收
- 生產(chǎn)環(huán)境(Production):真實(shí)用戶訪問,嚴(yán)格權(quán)限,關(guān)閉調(diào)試信息
同一份代碼,在三套環(huán)境里運(yùn)行時(shí),數(shù)據(jù)庫(kù)地址、API 密鑰、日志級(jí)別、第三方服務(wù) URL,都可能截然不同。如何讓代碼在不修改的情況下,自動(dòng)適應(yīng)不同環(huán)境?這是配置管理要解決的第一個(gè)核心問題。
1.2 安全與便利的博弈
把密碼寫在代碼里,方便但危險(xiǎn);把密碼加密存儲(chǔ),安全但復(fù)雜。配置管理始終在安全與便利之間尋找平衡點(diǎn)。
1.3 配置管理的黃金原則
在討論具體方案之前,先記住這個(gè)原則:"The Twelve-Factor App"第三條:將配置存儲(chǔ)在環(huán)境中,而不是代碼里。
二、第一層:環(huán)境變量——最簡(jiǎn)單的隔離
2.1 為什么首選環(huán)境變量
環(huán)境變量是配置管理的基石,理由有三:
- 天然隔離:不同環(huán)境設(shè)置不同的環(huán)境變量,代碼無需修改
- 安全存儲(chǔ):密鑰不會(huì)出現(xiàn)在版本控制系統(tǒng)中
- 云原生友好:Kubernetes、Docker、各大云平臺(tái)都原生支持
2.2 最簡(jiǎn)單的用法
import os
# 基礎(chǔ)用法:讀取環(huán)境變量
DATABASE_URL = os.environ["DATABASE_URL"] # 不存在則拋出 KeyError
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///dev.db") # 提供默認(rèn)值
# 類型轉(zhuǎn)換:環(huán)境變量都是字符串
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
PORT = int(os.environ.get("PORT", "8000"))
MAX_WORKERS = int(os.environ.get("MAX_WORKERS", "4"))
2.3 使用 python-dotenv 管理本地開發(fā)配置
生產(chǎn)環(huán)境通過 CI/CD 注入環(huán)境變量,但本地開發(fā)怎么辦?總不能每次都手動(dòng) export。python-dotenv 優(yōu)雅地解決了這個(gè)問題。
安裝:
pip install python-dotenv
項(xiàng)目根目錄創(chuàng)建 .env 文件:
# .env(絕對(duì)不能提交到 Git?。? DATABASE_URL=postgresql://user:password@localhost:5432/devdb REDIS_URL=redis://localhost:6379/0 SECRET_KEY=your-local-secret-key-never-use-in-production DEBUG=true LOG_LEVEL=DEBUG
.gitignore 中添加:
.env .env.local .env.*.local
代碼中加載:
from dotenv import load_dotenv
import os
# 加載 .env 文件(生產(chǎn)環(huán)境中 .env 文件不存在,不影響運(yùn)行)
load_dotenv()
DATABASE_URL = os.environ["DATABASE_URL"]
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
提供 .env.example 模板(可以提交到 Git):
# .env.example — 復(fù)制為 .env 并填寫真實(shí)值 DATABASE_URL=postgresql://user:password@host:5432/dbname REDIS_URL=redis://host:6379/0 SECRET_KEY=your-secret-key-here DEBUG=false LOG_LEVEL=INFO
2.4 環(huán)境變量的局限性
環(huán)境變量適合存儲(chǔ)少量、簡(jiǎn)單的鍵值對(duì),但面對(duì)復(fù)雜的嵌套配置時(shí),它開始力不從心:
# 嘗試用環(huán)境變量表達(dá)嵌套配置——丑陋且難以維護(hù) DATABASE_PRIMARY_HOST=db1.example.com DATABASE_PRIMARY_PORT=5432 DATABASE_REPLICA_HOST=db2.example.com DATABASE_REPLICA_PORT=5432
這時(shí)候,就需要第二層方案:配置文件。
三、第二層:配置文件——結(jié)構(gòu)化的力量
3.1 常見配置文件格式對(duì)比
| 格式 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 |
|---|---|---|---|
| INI | 簡(jiǎn)單易讀 | 不支持嵌套 | 簡(jiǎn)單項(xiàng)目 |
| JSON | 結(jié)構(gòu)清晰 | 不支持注釋 | API 配置 |
| YAML | 可讀性強(qiáng),支持注釋 | 縮進(jìn)敏感 | 大多數(shù)項(xiàng)目 |
| TOML | 語義明確 | 生態(tài)較新 | Python 項(xiàng)目(pyproject.toml) |
3.2 使用 YAML 配置文件
YAML 是目前最流行的配置文件格式,兼具可讀性和表達(dá)能力。
項(xiàng)目配置文件結(jié)構(gòu):
config/
├── base.yaml # 所有環(huán)境共享的基礎(chǔ)配置
├── development.yaml # 開發(fā)環(huán)境覆蓋配置
├── staging.yaml # 測(cè)試環(huán)境覆蓋配置
└── production.yaml # 生產(chǎn)環(huán)境覆蓋配置
base.yaml:
# 所有環(huán)境共享的默認(rèn)配置 app: name: "My Application" version: "1.0.0" timezone: "Asia/Shanghai" server: host: "0.0.0.0" port: 8000 workers: 4 logging: level: "INFO" format: "json" cache: ttl: 3600 max_size: 1000
development.yaml:
# 僅覆蓋開發(fā)環(huán)境特有的配置 server: port: 8080 logging: level: "DEBUG" format: "console" # 開發(fā)環(huán)境用人類可讀格式 database: url: "postgresql://dev:dev@localhost:5432/myapp_dev" pool_size: 2 echo: true # 打印 SQL 語句 cache: ttl: 60 # 開發(fā)環(huán)境緩存時(shí)間短
production.yaml:
server: workers: 16 # 生產(chǎn)環(huán)境更多 worker logging: level: "WARNING" # 生產(chǎn)環(huán)境減少日志量 database: pool_size: 20 pool_timeout: 30 echo: false
3.3 實(shí)現(xiàn)配置合并加載器
import yaml
import os
from pathlib import Path
from typing import Any
def deep_merge(base: dict, override: dict) -> dict:
"""
深度合并兩個(gè)字典,override 的值會(huì)覆蓋 base 的值
支持嵌套字典的遞歸合并
"""
result = base.copy()
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = deep_merge(result[key], value)
else:
result[key] = value
return result
def load_config() -> dict:
"""
按優(yōu)先級(jí)加載配置:
base.yaml < {env}.yaml < 環(huán)境變量
"""
config_dir = Path(__file__).parent / "config"
env = os.environ.get("APP_ENV", "development")
# 1. 加載基礎(chǔ)配置
base_path = config_dir / "base.yaml"
with open(base_path) as f:
config = yaml.safe_load(f)
# 2. 加載環(huán)境特定配置(覆蓋基礎(chǔ)配置)
env_path = config_dir / f"{env}.yaml"
if env_path.exists():
with open(env_path) as f:
env_config = yaml.safe_load(f) or {}
config = deep_merge(config, env_config)
# 3. 環(huán)境變量中的配置優(yōu)先級(jí)最高
# 支持 APP__DATABASE__URL 形式的嵌套鍵
for key, value in os.environ.items():
if key.startswith("APP__"):
keys = key[5:].lower().split("__")
nested = config
for k in keys[:-1]:
nested = nested.setdefault(k, {})
nested[keys[-1]] = value
return config
# 全局配置單例
_config = None
def get_config() -> dict:
global _config
if _config is None:
_config = load_config()
return _config
使用示例:
from config_loader import get_config config = get_config() # 訪問配置 db_url = config["database"]["url"] log_level = config["logging"]["level"] server_port = config["server"]["port"]
3.4 使用 Pydantic 實(shí)現(xiàn)類型安全的配置
原始字典沒有類型檢查,容易出錯(cuò)。用 Pydantic 定義強(qiáng)類型配置模型,在應(yīng)用啟動(dòng)時(shí)就能發(fā)現(xiàn)配置錯(cuò)誤:
from pydantic import BaseModel, validator, Field
from pydantic_settings import BaseSettings
from typing import Optional
import os
class DatabaseConfig(BaseModel):
url: str
pool_size: int = 10
pool_timeout: int = 30
echo: bool = False
@validator("pool_size")
def validate_pool_size(cls, v):
if v < 1 or v > 100:
raise ValueError("pool_size 必須在 1-100 之間")
return v
class ServerConfig(BaseModel):
host: str = "0.0.0.0"
port: int = Field(8000, ge=1, le=65535)
workers: int = Field(4, ge=1)
class LoggingConfig(BaseModel):
level: str = "INFO"
format: str = "json"
@validator("level")
def validate_level(cls, v):
valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
if v.upper() not in valid_levels:
raise ValueError(f"日志級(jí)別必須是 {valid_levels} 之一")
return v.upper()
class AppConfig(BaseSettings):
"""
頂層配置模型
自動(dòng)從環(huán)境變量讀取(優(yōu)先級(jí)高于默認(rèn)值)
"""
env: str = Field("development", env="APP_ENV")
database: DatabaseConfig
server: ServerConfig = ServerConfig()
logging: LoggingConfig = LoggingConfig()
class Config:
env_prefix = "APP_"
env_nested_delimiter = "__"
# 使用方式
def create_config() -> AppConfig:
raw_config = load_config() # 從 YAML 文件加載
return AppConfig(**raw_config)
# 全局配置
settings = create_config()
# 類型安全的訪問
print(settings.database.url) # str
print(settings.server.port) # int,不需要類型轉(zhuǎn)換
print(settings.logging.level) # 已驗(yàn)證為合法值
四、第三層:配置中心——分布式系統(tǒng)的救星
當(dāng)系統(tǒng)演進(jìn)為微服務(wù)架構(gòu),配置文件方案開始暴露短板:
- 配置變更需要重新部署:改一個(gè)參數(shù),所有服務(wù)都要重啟
- 多服務(wù)配置不一致:十個(gè)服務(wù),十套配置,維護(hù)噩夢(mèng)
- 敏感信息難以集中管控:數(shù)據(jù)庫(kù)密碼散落各處
這時(shí),配置中心登場(chǎng)了。
4.1 HashiCorp Vault:密鑰管理的王者
Vault 專門用于管理敏感配置(密鑰、證書、API Token):
import hvac
import os
from functools import lru_cache
class VaultConfigProvider:
"""從 HashiCorp Vault 讀取敏感配置"""
def __init__(self):
self.client = hvac.Client(
url=os.environ["VAULT_ADDR"],
token=os.environ["VAULT_TOKEN"]
)
def get_secret(self, path: str) -> dict:
"""讀取 KV v2 格式的密鑰"""
response = self.client.secrets.kv.v2.read_secret_version(
path=path,
mount_point="secret"
)
return response["data"]["data"]
def get_database_credentials(self) -> dict:
"""獲取動(dòng)態(tài)數(shù)據(jù)庫(kù)憑證(每次調(diào)用都獲取新的臨時(shí)憑證)"""
response = self.client.secrets.database.generate_credentials(
name="my-app-role"
)
return {
"username": response["data"]["username"],
"password": response["data"]["password"],
"lease_id": response["lease_id"],
"lease_duration": response["lease_duration"]
}
@lru_cache(maxsize=None)
def get_vault_provider() -> VaultConfigProvider:
return VaultConfigProvider()
# 在應(yīng)用啟動(dòng)時(shí)獲取敏感配置
vault = get_vault_provider()
# 獲取數(shù)據(jù)庫(kù)密碼(不再硬編碼)
db_secret = vault.get_secret("myapp/database")
DATABASE_URL = f"postgresql://{db_secret['username']}:{db_secret['password']}@{db_secret['host']}/myapp"
4.2 動(dòng)態(tài)配置:運(yùn)行時(shí)熱更新
某些配置需要在不重啟服務(wù)的情況下動(dòng)態(tài)調(diào)整(如限流閾值、功能開關(guān))。使用 Redis 或 etcd 實(shí)現(xiàn)動(dòng)態(tài)配置:
import redis
import json
import threading
from typing import Any, Callable
class DynamicConfig:
"""
基于 Redis 的動(dòng)態(tài)配置,支持熱更新
使用 Redis Pub/Sub 監(jiān)聽配置變更
"""
def __init__(self, redis_url: str):
self.redis = redis.from_url(redis_url)
self._cache = {}
self._listeners: dict[str, list[Callable]] = {}
self._start_listener()
def get(self, key: str, default: Any = None) -> Any:
"""獲取配置值(優(yōu)先從本地緩存讀?。?""
if key not in self._cache:
value = self.redis.get(f"config:{key}")
if value:
self._cache[key] = json.loads(value)
else:
return default
return self._cache.get(key, default)
def set(self, key: str, value: Any) -> None:
"""設(shè)置配置值,并通知所有實(shí)例"""
self.redis.set(f"config:{key}", json.dumps(value))
self.redis.publish("config:changes", json.dumps({"key": key, "value": value}))
def on_change(self, key: str, callback: Callable) -> None:
"""注冊(cè)配置變更回調(diào)"""
if key not in self._listeners:
self._listeners[key] = []
self._listeners[key].append(callback)
def _start_listener(self) -> None:
"""在后臺(tái)線程監(jiān)聽配置變更"""
def listen():
pubsub = self.redis.pubsub()
pubsub.subscribe("config:changes")
for message in pubsub.listen():
if message["type"] == "message":
data = json.loads(message["data"])
key = data["key"]
self._cache[key] = data["value"]
# 觸發(fā)回調(diào)
for callback in self._listeners.get(key, []):
callback(data["value"])
thread = threading.Thread(target=listen, daemon=True)
thread.start()
# 使用示例
dynamic_config = DynamicConfig(redis_url="redis://localhost:6379/1")
# 注冊(cè)限流閾值變更回調(diào)
def on_rate_limit_change(new_value):
print(f"限流閾值已更新為: {new_value} 次/分鐘")
# 實(shí)時(shí)更新限流器配置
dynamic_config.on_change("rate_limit_per_minute", on_rate_limit_change)
# 讀取動(dòng)態(tài)配置
rate_limit = dynamic_config.get("rate_limit_per_minute", default=100)
feature_enabled = dynamic_config.get("feature_new_ui", default=False)
# 運(yùn)維人員可以在不重啟服務(wù)的情況下調(diào)整
# dynamic_config.set("rate_limit_per_minute", 200)
五、綜合實(shí)戰(zhàn):構(gòu)建完整的配置管理體系
5.1 分層配置架構(gòu)圖
┌─────────────────────────────────────────────────┐
│ 應(yīng)用配置層次 │
├─────────────────────────────────────────────────┤
│ 優(yōu)先級(jí)(從高到低) │
│ │
│ 1. 命令行參數(shù) --port=8080 │
│ ↓ │
│ 2. 環(huán)境變量 APP__SERVER__PORT=8080 │
│ ↓ │
│ 3. 配置中心 Vault / etcd / Redis │
│ ↓ │
│ 4. 環(huán)境配置文件 config/production.yaml │
│ ↓ │
│ 5. 基礎(chǔ)配置文件 config/base.yaml │
│ ↓ │
│ 6. 代碼默認(rèn)值 port: int = 8000 │
└─────────────────────────────────────────────────┘
5.2 完整的配置管理類
import os
import yaml
from pathlib import Path
from functools import lru_cache
from pydantic import BaseModel, validator
from pydantic_settings import BaseSettings
from typing import Optional
class DatabaseSettings(BaseModel):
url: str = "sqlite:///./dev.db"
pool_size: int = 5
echo: bool = False
class RedisSettings(BaseModel):
url: str = "redis://localhost:6379/0"
max_connections: int = 10
class AppSettings(BaseSettings):
# 基礎(chǔ)信息
app_name: str = "MyApp"
app_env: str = "development"
debug: bool = False
secret_key: str = "change-this-in-production"
# 子配置
database: DatabaseSettings = DatabaseSettings()
redis: RedisSettings = RedisSettings()
# 功能開關(guān)
feature_new_dashboard: bool = False
class Config:
env_file = ".env"
env_prefix = "APP_"
env_nested_delimiter = "__"
@validator("secret_key")
def validate_secret_key(cls, v, values):
env = values.get("app_env", "development")
if env == "production" and v == "change-this-in-production":
raise ValueError("生產(chǎn)環(huán)境必須設(shè)置真實(shí)的 SECRET_KEY!")
return v
@validator("app_env")
def validate_env(cls, v):
allowed = {"development", "staging", "production", "test"}
if v not in allowed:
raise ValueError(f"app_env 必須是 {allowed} 之一")
return v
def is_production(self) -> bool:
return self.app_env == "production"
def is_development(self) -> bool:
return self.app_env == "development"
@lru_cache(maxsize=1)
def get_settings() -> AppSettings:
"""
獲取全局配置單例(使用 lru_cache 確保只初始化一次)
在測(cè)試中可以通過 get_settings.cache_clear() 重置
"""
return AppSettings()
# 在應(yīng)用的任何地方使用
settings = get_settings()
print(f"運(yùn)行環(huán)境: {settings.app_env}")
print(f"數(shù)據(jù)庫(kù): {settings.database.url}")
print(f"調(diào)試模式: {settings.debug}")
5.3 測(cè)試中的配置隔離
# tests/conftest.py
import pytest
from unittest.mock import patch
from config import get_settings, AppSettings
@pytest.fixture
def test_settings():
"""提供測(cè)試專用的配置"""
test_config = AppSettings(
app_env="test",
debug=True,
database={"url": "sqlite:///./test.db", "echo": True},
secret_key="test-secret-key"
)
return test_config
@pytest.fixture(autouse=True)
def override_settings(test_settings):
"""自動(dòng)替換所有測(cè)試中的配置"""
# 清除 lru_cache 并替換
get_settings.cache_clear()
with patch("config.get_settings", return_value=test_settings):
yield
get_settings.cache_clear()
六、最佳實(shí)踐清單
在我經(jīng)歷過數(shù)十個(gè) Python 項(xiàng)目之后,總結(jié)出以下配置管理鐵律:
安全規(guī)范:
- 敏感信息(密碼、API Key)只能通過環(huán)境變量或配置中心注入,絕不寫入代碼或配置文件
.env文件必須加入.gitignore,倉(cāng)庫(kù)中只保留.env.example- 定期輪換密鑰,使用 Vault 的動(dòng)態(tài)憑證功能減少泄露風(fēng)險(xiǎn)
結(jié)構(gòu)規(guī)范:
- 使用 Pydantic 定義配置模型,應(yīng)用啟動(dòng)時(shí)進(jìn)行驗(yàn)證,讓配置錯(cuò)誤在部署階段就暴露
- 配置文件按環(huán)境分層,base → 環(huán)境專屬,使用深度合并
- 配置值通過依賴注入傳遞,避免在代碼深處直接調(diào)用
os.environ
運(yùn)維規(guī)范:
- 提供
APP_ENV變量明確標(biāo)識(shí)環(huán)境,防止誤操作 - 關(guān)鍵配置加載失敗時(shí),應(yīng)用應(yīng)拒絕啟動(dòng)并給出明確錯(cuò)誤信息
- 建立配置變更審計(jì)日志,每次生產(chǎn)配置修改都有記錄
七、總結(jié)與展望
配置管理是 Python 工程化體系中的基礎(chǔ)設(shè)施,它的質(zhì)量直接影響系統(tǒng)的安全性、可維護(hù)性和團(tuán)隊(duì)協(xié)作效率。
我們從三個(gè)層次進(jìn)行了深入探討:環(huán)境變量適合簡(jiǎn)單鍵值對(duì)和敏感信息的隔離;配置文件適合結(jié)構(gòu)化的靜態(tài)配置,通過 Pydantic 實(shí)現(xiàn)類型安全;配置中心適合分布式系統(tǒng)中需要集中管控或動(dòng)態(tài)更新的配置。
三者不是互斥的,而是互補(bǔ)的。成熟的項(xiàng)目往往三者并用,根據(jù)配置的性質(zhì)選擇合適的存儲(chǔ)方式。
配置管理沒有銀彈,但有一條永恒的原則:讓配置錯(cuò)誤盡早暴露,而不是在凌晨?jī)牲c(diǎn)讓你從睡夢(mèng)中驚醒。
你在項(xiàng)目中是如何管理不同環(huán)境的配置的?有沒有遇到過因?yàn)榕渲没靵y引發(fā)的生產(chǎn)事故?歡迎在評(píng)論區(qū)分享你的經(jīng)驗(yàn)和教訓(xùn),讓我們一起把這道坎踩實(shí)。
以上就是從環(huán)境變量到配置中心帶你掌握Python多環(huán)境配置的詳細(xì)內(nèi)容,更多關(guān)于Python多環(huán)境配置的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
讓python json encode datetime類型
python2.6+ 自帶的json模塊,不支持datetime的json encode,每次都需要手動(dòng)轉(zhuǎn)為字符串,很累人,我們可以自己封裝一個(gè)簡(jiǎn)單的方法處理此問題。2010-12-12
python異步的ASGI與Fast Api實(shí)現(xiàn)
本文主要介紹了python異步的ASGI與Fast Api實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-07-07
Python使用asyncio實(shí)現(xiàn)異步操作的示例
本文主要介紹了Python使用asyncio實(shí)現(xiàn)異步操作的示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-01-01
Python greenlet實(shí)現(xiàn)原理和使用示例
這篇文章主要介紹了Python greenlet實(shí)現(xiàn)原理和使用示例,greenlet是Python中的一個(gè)并行處理庫(kù),需要的朋友可以參考下2014-09-09
python中如何正確使用正則表達(dá)式的詳細(xì)模式(Verbose mode expression)
許多程序設(shè)計(jì)語言都支持利用正則表達(dá)式進(jìn)行字符串操作,python自然也不例外,下面這篇文章主要給大家介紹了關(guān)于在python中如何正確使用正則表達(dá)式的詳細(xì)模式(Verbose mode expression)的相關(guān)資料,需要的朋友可以參考借鑒,下面來一起看看吧。2017-11-11

