springbootAI 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spring/__init__.py +66 -0
- spring/ai/__init__.py +78 -0
- spring/ai/advisors.py +139 -0
- spring/ai/annotations.py +74 -0
- spring/ai/autoconfig.py +481 -0
- spring/ai/core.py +391 -0
- spring/ai/etl.py +188 -0
- spring/ai/memory.py +109 -0
- spring/ai/observability.py +129 -0
- spring/ai/providers.py +789 -0
- spring/ai/resilience.py +258 -0
- spring/ai/tools.py +106 -0
- spring/ai/vectorstore.py +303 -0
- spring/annotations/__init__.py +188 -0
- spring/annotations/cache.py +126 -0
- spring/annotations/cloud.py +207 -0
- spring/annotations/conditional.py +272 -0
- spring/annotations/core.py +864 -0
- spring/annotations/messaging.py +107 -0
- spring/aop/__init__.py +4 -0
- spring/aop/cloud_aop.py +404 -0
- spring/aop/comprehensive_aop.py +1015 -0
- spring/aop/method_interceptor.py +19 -0
- spring/aop/proxy_factory.py +55 -0
- spring/cloud/__init__.py +76 -0
- spring/cloud/discovery.py +364 -0
- spring/cloud/feign.py +469 -0
- spring/cloud/gateway.py +452 -0
- spring/cloud/load_balancer.py +149 -0
- spring/cloud/seata.py +557 -0
- spring/cloud/sentinel.py +525 -0
- spring/cloud/tracer.py +337 -0
- spring/config/__init__.py +21 -0
- spring/config/binding.py +206 -0
- spring/config/config_loader.py +405 -0
- spring/context/__init__.py +13 -0
- spring/context/application_context.py +589 -0
- spring/context/bean_definition.py +70 -0
- spring/context/bean_factory.py +1052 -0
- spring/context/registry.py +58 -0
- spring/context/scanner.py +106 -0
- spring/core/__init__.py +3 -0
- spring/core/graceful_shutdown.py +196 -0
- spring/core/typing_utils.py +50 -0
- spring/csv/__init__.py +52 -0
- spring/csv/annotations.py +402 -0
- spring/csv/converters.py +69 -0
- spring/csv/easy_csv.py +95 -0
- spring/csv/exceptions.py +27 -0
- spring/csv/reader.py +195 -0
- spring/csv/writer.py +155 -0
- spring/data/__init__.py +54 -0
- spring/data/page.py +181 -0
- spring/data/repository.py +274 -0
- spring/data/specification.py +228 -0
- spring/datasource/__init__.py +66 -0
- spring/datasource/annotations.py +133 -0
- spring/datasource/context.py +69 -0
- spring/datasource/dynamic.py +148 -0
- spring/event/__init__.py +7 -0
- spring/event/publisher.py +69 -0
- spring/excel/__init__.py +51 -0
- spring/excel/annotations.py +405 -0
- spring/excel/converters.py +231 -0
- spring/excel/easy_excel.py +94 -0
- spring/excel/exceptions.py +31 -0
- spring/excel/reader.py +254 -0
- spring/excel/style.py +95 -0
- spring/excel/writer.py +197 -0
- spring/i18n/__init__.py +97 -0
- spring/i18n/accessor.py +94 -0
- spring/i18n/auto_config.py +177 -0
- spring/i18n/holder.py +106 -0
- spring/i18n/locale.py +152 -0
- spring/i18n/locale_resolver.py +367 -0
- spring/i18n/message_source.py +250 -0
- spring/i18n/middleware.py +79 -0
- spring/i18n/properties.py +168 -0
- spring/i18n/sources.py +255 -0
- spring/logging/__init__.py +1 -0
- spring/logging/loguru_logger.py +228 -0
- spring/main.py +378 -0
- spring/messaging/__init__.py +1 -0
- spring/messaging/rabbitmq.py +302 -0
- spring/monitoring/__init__.py +1 -0
- spring/monitoring/prometheus.py +199 -0
- spring/orm/__init__.py +258 -0
- spring/orm/database.py +222 -0
- spring/orm/ddl_auto.py +1217 -0
- spring/orm/migration.py +419 -0
- spring/orm/mybatis_integration.py +400 -0
- spring/orm/pymybatis/__init__.py +86 -0
- spring/orm/pymybatis/annotations/__init__.py +30 -0
- spring/orm/pymybatis/annotations/annotations.py +332 -0
- spring/orm/pymybatis/cache/__init__.py +47 -0
- spring/orm/pymybatis/cache/cache.py +371 -0
- spring/orm/pymybatis/cache/redis_cache.py +434 -0
- spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
- spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
- spring/orm/pymybatis/configuration.py +525 -0
- spring/orm/pymybatis/core/__init__.py +10 -0
- spring/orm/pymybatis/core/sql_session.py +1382 -0
- spring/orm/pymybatis/core/sql_session_factory.py +76 -0
- spring/orm/pymybatis/dialect/__init__.py +9 -0
- spring/orm/pymybatis/dialect/dialect.py +445 -0
- spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
- spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
- spring/orm/pymybatis/interceptor/__init__.py +31 -0
- spring/orm/pymybatis/interceptor/interceptor.py +427 -0
- spring/orm/pymybatis/mapper/__init__.py +9 -0
- spring/orm/pymybatis/mapper/mapper.py +540 -0
- spring/orm/pymybatis/metrics/__init__.py +41 -0
- spring/orm/pymybatis/metrics/metrics.py +595 -0
- spring/orm/pymybatis/pool/__init__.py +9 -0
- spring/orm/pymybatis/pool/connection_pool.py +711 -0
- spring/orm/pymybatis/security/__init__.py +19 -0
- spring/orm/pymybatis/security/access_control.py +415 -0
- spring/orm/pymybatis/security/password_encoder.py +293 -0
- spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
- spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
- spring/orm/pymybatis/transaction/__init__.py +9 -0
- spring/orm/pymybatis/transaction/transaction.py +288 -0
- spring/orm/pymybatis/type_handler/__init__.py +37 -0
- spring/orm/pymybatis/type_handler/type_handler.py +473 -0
- spring/orm/pymybatis/version.py +9 -0
- spring/orm/pymybatis/xml_parser/__init__.py +9 -0
- spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
- spring/retry/__init__.py +12 -0
- spring/retry/retry_annotations.py +71 -0
- spring/retry/retry_decorator.py +155 -0
- spring/scheduling/__init__.py +3 -0
- spring/scheduling/scheduler.py +389 -0
- spring/security/__init__.py +39 -0
- spring/security/jwt_utils.py +281 -0
- spring/security/replay_protection.py +206 -0
- spring/security/secret_manager.py +226 -0
- spring/security/security_aop.py +248 -0
- spring/security/security_context.py +172 -0
- spring/test/__init__.py +45 -0
- spring/test/slicing.py +341 -0
- spring/tracing/__init__.py +11 -0
- spring/tracing/skywalking.py +229 -0
- spring/tx/__init__.py +52 -0
- spring/tx/events.py +172 -0
- spring/tx/synchronization.py +143 -0
- spring/utils/__init__.py +5 -0
- spring/utils/banner.py +32 -0
- spring/utils/logger.py +73 -0
- spring/utils/redis_client.py +526 -0
- spring/validation/__init__.py +55 -0
- spring/validation/aop.py +141 -0
- spring/validation/constraints.py +357 -0
- spring/validation/exceptions.py +55 -0
- spring/validation/validator.py +139 -0
- spring/web/__init__.py +12 -0
- spring/web/actuator.py +319 -0
- spring/web/exception_handler.py +61 -0
- spring/web/health.py +399 -0
- spring/web/interceptor.py +91 -0
- spring/web/result.py +44 -0
- spring/web/swagger.py +601 -0
- spring/web/web_context.py +755 -0
- spring/websocket/__init__.py +86 -0
- spring/websocket/annotations.py +169 -0
- spring/websocket/broker.py +238 -0
- spring/websocket/exceptions.py +26 -0
- spring/websocket/handler.py +243 -0
- spring/websocket/router.py +526 -0
- spring/websocket/session.py +216 -0
- springbootai-1.8.0.dist-info/METADATA +2796 -0
- springbootai-1.8.0.dist-info/RECORD +175 -0
- springbootai-1.8.0.dist-info/WHEEL +5 -0
- springbootai-1.8.0.dist-info/entry_points.txt +2 -0
- springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
- springbootai-1.8.0.dist-info/top_level.txt +1 -0
spring/cloud/gateway.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""
|
|
2
|
+
轻量异步 API 网关 (Embedded API Gateway)
|
|
3
|
+
|
|
4
|
+
无需 Spring Cloud Gateway / WebFlux 的内嵌 API 网关,基于异步 ASGI 反向代理:
|
|
5
|
+
- 路由转发(service-id -> instance URL via discovery)
|
|
6
|
+
- 负载均衡(支持轮询/随机/权重)
|
|
7
|
+
- 全局过滤器(认证、日志、追踪头注入、限流)
|
|
8
|
+
- 路径重写(StripPrefix、PrefixPath)
|
|
9
|
+
- 熔断集成(依赖 Sentinel)
|
|
10
|
+
|
|
11
|
+
Usage (Starlette/FastAPI-based):
|
|
12
|
+
from spring.cloud.gateway import GatewayRouter
|
|
13
|
+
|
|
14
|
+
gateway = GatewayRouter(discovery_client=discovery_client)
|
|
15
|
+
gateway.route("/users/**", service_id="user-service", strip_prefix=True)
|
|
16
|
+
gateway.route("/orders/**", service_id="order-service")
|
|
17
|
+
app.add_route("/api/{path:path}", gateway.handle_asgi,
|
|
18
|
+
methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import time
|
|
22
|
+
import logging
|
|
23
|
+
import threading
|
|
24
|
+
import re
|
|
25
|
+
import random
|
|
26
|
+
import inspect
|
|
27
|
+
from typing import Dict, List, Optional, Callable, Any, Tuple
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
|
|
30
|
+
import httpx
|
|
31
|
+
from starlette.requests import Request
|
|
32
|
+
from starlette.responses import JSONResponse, Response
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger("Spring.Cloud.Gateway")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Route:
|
|
39
|
+
"""网关路由定义"""
|
|
40
|
+
id: str
|
|
41
|
+
path: str # 路径匹配模式,支持 ** 通配符
|
|
42
|
+
service_id: str = "" # 目标服务ID(通过发现获取)
|
|
43
|
+
uri: str = "" # 直接目标URI(优先级高于service_id)
|
|
44
|
+
strip_prefix: bool = False # 是否去除前缀
|
|
45
|
+
prefix: str = "" # 添加前缀
|
|
46
|
+
filters: List[str] = field(default_factory=list)
|
|
47
|
+
predicates: Dict[str, Any] = field(default_factory=dict)
|
|
48
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
49
|
+
enabled: bool = True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class FilterContext:
|
|
54
|
+
"""过滤器上下文"""
|
|
55
|
+
route: Route
|
|
56
|
+
request_path: str
|
|
57
|
+
request_headers: Dict[str, str]
|
|
58
|
+
request_method: str
|
|
59
|
+
request_query: Dict[str, str]
|
|
60
|
+
response_status: int = 0
|
|
61
|
+
response_headers: Dict[str, str] = field(default_factory=dict)
|
|
62
|
+
attributes: Dict[str, Any] = field(default_factory=dict)
|
|
63
|
+
start_time: float = 0.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class GatewayFilter:
|
|
67
|
+
"""网关过滤器基类"""
|
|
68
|
+
def pre_filter(self, ctx: FilterContext) -> bool:
|
|
69
|
+
"""前置过滤,返回False则终止请求"""
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
def post_filter(self, ctx: FilterContext):
|
|
73
|
+
"""后置过滤"""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AuthenticationFilter(GatewayFilter):
|
|
78
|
+
"""认证过滤器:检查JWT/Token"""
|
|
79
|
+
def __init__(self, token_header: str = "Authorization", exclude_paths: List[str] = None):
|
|
80
|
+
self.token_header = token_header
|
|
81
|
+
self.exclude_paths = exclude_paths or ["/login", "/health", "/actuator"]
|
|
82
|
+
|
|
83
|
+
def pre_filter(self, ctx: FilterContext) -> bool:
|
|
84
|
+
for ep in self.exclude_paths:
|
|
85
|
+
if ctx.request_path.startswith(ep):
|
|
86
|
+
return True
|
|
87
|
+
token = next(
|
|
88
|
+
(value for key, value in ctx.request_headers.items()
|
|
89
|
+
if key.lower() == self.token_header.lower()),
|
|
90
|
+
"",
|
|
91
|
+
)
|
|
92
|
+
if not token:
|
|
93
|
+
ctx.response_status = 401
|
|
94
|
+
ctx.response_headers["X-Gateway-Error"] = "Missing Authorization"
|
|
95
|
+
return False
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TracingFilter(GatewayFilter):
|
|
100
|
+
"""追踪头注入过滤器"""
|
|
101
|
+
def pre_filter(self, ctx: FilterContext) -> bool:
|
|
102
|
+
try:
|
|
103
|
+
from spring.cloud.tracer import get_tracer
|
|
104
|
+
tracer = get_tracer()
|
|
105
|
+
tp = tracer.get_traceparent_header()
|
|
106
|
+
if tp:
|
|
107
|
+
ctx.request_headers['traceparent'] = tp
|
|
108
|
+
cur = tracer.get_current_span()
|
|
109
|
+
if cur:
|
|
110
|
+
ctx.request_headers['X-B3-TraceId'] = cur.trace_id
|
|
111
|
+
ctx.request_headers['X-B3-SpanId'] = cur.span_id
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class RateLimitFilter(GatewayFilter):
|
|
118
|
+
"""网关限流过滤器(使用Sentinel引擎)"""
|
|
119
|
+
def __init__(self, default_qps: float = 500.0):
|
|
120
|
+
self.default_qps = default_qps
|
|
121
|
+
|
|
122
|
+
def pre_filter(self, ctx: FilterContext) -> bool:
|
|
123
|
+
try:
|
|
124
|
+
from spring.cloud.sentinel import sentinel_engine
|
|
125
|
+
resource = f"gateway:{ctx.route.id}:{ctx.request_method}"
|
|
126
|
+
try:
|
|
127
|
+
sentinel_engine.entry(resource, args=(), kwargs={})
|
|
128
|
+
# 简化:不维持entry对象,仅做QPS检查
|
|
129
|
+
except Exception:
|
|
130
|
+
ctx.response_status = 429
|
|
131
|
+
ctx.response_headers["X-Gateway-Error"] = "Rate Limited"
|
|
132
|
+
return False
|
|
133
|
+
except ImportError:
|
|
134
|
+
pass
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class LoggingFilter(GatewayFilter):
|
|
139
|
+
"""访问日志过滤器"""
|
|
140
|
+
def pre_filter(self, ctx: FilterContext) -> bool:
|
|
141
|
+
ctx.start_time = time.monotonic()
|
|
142
|
+
logger.info(f"[Gateway] {ctx.request_method} {ctx.request_path} -> {ctx.route.service_id or ctx.route.uri}")
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
def post_filter(self, ctx: FilterContext):
|
|
146
|
+
duration = (time.monotonic() - ctx.start_time) * 1000
|
|
147
|
+
logger.info(f"[Gateway] {ctx.request_method} {ctx.request_path} "
|
|
148
|
+
f"status={ctx.response_status} duration={duration:.2f}ms")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class LoadBalancerStrategy:
|
|
152
|
+
"""负载均衡策略"""
|
|
153
|
+
@staticmethod
|
|
154
|
+
def round_robin(instances: List[dict]) -> Optional[dict]:
|
|
155
|
+
if not instances:
|
|
156
|
+
return None
|
|
157
|
+
idx = int(time.time() * 1000) % len(instances)
|
|
158
|
+
return instances[idx]
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def random_choice(instances: List[dict]) -> Optional[dict]:
|
|
162
|
+
if not instances:
|
|
163
|
+
return None
|
|
164
|
+
return random.choice(instances)
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def weighted(instances: List[dict]) -> Optional[dict]:
|
|
168
|
+
if not instances:
|
|
169
|
+
return None
|
|
170
|
+
weights = [inst.get('weight', 1) for inst in instances]
|
|
171
|
+
total = sum(weights)
|
|
172
|
+
r = random.uniform(0, total)
|
|
173
|
+
upto = 0
|
|
174
|
+
for inst, w in zip(instances, weights):
|
|
175
|
+
if upto + w >= r:
|
|
176
|
+
return inst
|
|
177
|
+
upto += w
|
|
178
|
+
return instances[0]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class GatewayRouter:
|
|
182
|
+
"""
|
|
183
|
+
API 网关路由
|
|
184
|
+
|
|
185
|
+
作为异步 Starlette/FastAPI endpoint 使用。上游 I/O 不会阻塞事件循环。
|
|
186
|
+
|
|
187
|
+
Usage:
|
|
188
|
+
gateway = GatewayRouter(discovery_client=nacos_discovery)
|
|
189
|
+
gateway.route("/api/users/**", "user-service", strip_prefix=True)
|
|
190
|
+
app.add_route("/api/{path:path}", gateway.handle_asgi, methods=["GET","POST","PUT","DELETE"])
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
_HOP_BY_HOP_HEADERS = {
|
|
194
|
+
'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',
|
|
195
|
+
'te', 'trailer', 'transfer-encoding', 'upgrade',
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
def __init__(self, discovery_client=None, default_filters: List[GatewayFilter] = None,
|
|
199
|
+
timeout: float = 10.0, max_body_size: int = 10 * 1024 * 1024,
|
|
200
|
+
transport: Optional[httpx.AsyncBaseTransport] = None):
|
|
201
|
+
self.discovery = discovery_client
|
|
202
|
+
self.routes: List[Route] = []
|
|
203
|
+
self.filters: List[GatewayFilter] = (
|
|
204
|
+
list(default_filters) if default_filters is not None
|
|
205
|
+
else [LoggingFilter(), TracingFilter()]
|
|
206
|
+
)
|
|
207
|
+
self._rr_counters: Dict[str, int] = {}
|
|
208
|
+
self._rr_lock = threading.Lock()
|
|
209
|
+
self._path_pattern_cache: Dict[str, re.Pattern] = {}
|
|
210
|
+
self.timeout = float(timeout)
|
|
211
|
+
self.max_body_size = int(max_body_size)
|
|
212
|
+
self._transport = transport
|
|
213
|
+
self._client: Optional[httpx.AsyncClient] = None
|
|
214
|
+
self._client_lock = threading.Lock()
|
|
215
|
+
|
|
216
|
+
def install(self, app, path: str = "/{path:path}",
|
|
217
|
+
methods: Optional[List[str]] = None) -> "GatewayRouter":
|
|
218
|
+
"""Register the gateway route and its HTTP-client shutdown hook."""
|
|
219
|
+
app.add_api_route(
|
|
220
|
+
path,
|
|
221
|
+
self.handle_asgi,
|
|
222
|
+
methods=methods or ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
223
|
+
)
|
|
224
|
+
app.router.add_event_handler("shutdown", self.aclose)
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
def _get_client(self) -> httpx.AsyncClient:
|
|
228
|
+
# Construction has no await points. A short process-local lock avoids
|
|
229
|
+
# leaking duplicate connection pools during the first concurrent hit.
|
|
230
|
+
with self._client_lock:
|
|
231
|
+
if self._client is None:
|
|
232
|
+
self._client = httpx.AsyncClient(
|
|
233
|
+
timeout=httpx.Timeout(self.timeout),
|
|
234
|
+
follow_redirects=False,
|
|
235
|
+
transport=self._transport,
|
|
236
|
+
)
|
|
237
|
+
return self._client
|
|
238
|
+
|
|
239
|
+
async def aclose(self) -> None:
|
|
240
|
+
with self._client_lock:
|
|
241
|
+
client = self._client
|
|
242
|
+
self._client = None
|
|
243
|
+
if client is not None:
|
|
244
|
+
await client.aclose()
|
|
245
|
+
|
|
246
|
+
def add_filter(self, flt: GatewayFilter):
|
|
247
|
+
self.filters.append(flt)
|
|
248
|
+
|
|
249
|
+
def route(self, path: str, service_id: str = "", uri: str = "",
|
|
250
|
+
strip_prefix: bool = False, prefix: str = "",
|
|
251
|
+
route_id: str = "", filters: List[str] = None,
|
|
252
|
+
**predicates) -> Route:
|
|
253
|
+
"""添加路由"""
|
|
254
|
+
rid = route_id or f"route_{len(self.routes) + 1}"
|
|
255
|
+
r = Route(
|
|
256
|
+
id=rid,
|
|
257
|
+
path=path,
|
|
258
|
+
service_id=service_id,
|
|
259
|
+
uri=uri,
|
|
260
|
+
strip_prefix=strip_prefix,
|
|
261
|
+
prefix=prefix,
|
|
262
|
+
filters=filters or [],
|
|
263
|
+
predicates=predicates,
|
|
264
|
+
)
|
|
265
|
+
self.routes.append(r)
|
|
266
|
+
self._path_pattern_cache[path] = self._compile_pattern(path)
|
|
267
|
+
logger.info(f"[Gateway] Route added: {path} -> {service_id or uri}")
|
|
268
|
+
return r
|
|
269
|
+
|
|
270
|
+
def _compile_pattern(self, pattern: str) -> re.Pattern:
|
|
271
|
+
"""编译路径通配符为正则表达式"""
|
|
272
|
+
regex = re.escape(pattern).replace(r'\*\*', '.*').replace(r'\*', '[^/]*')
|
|
273
|
+
return re.compile(f'^{regex}$')
|
|
274
|
+
|
|
275
|
+
def match_route(self, request_path: str) -> Optional[Route]:
|
|
276
|
+
"""匹配路由"""
|
|
277
|
+
for r in self.routes:
|
|
278
|
+
if not r.enabled:
|
|
279
|
+
continue
|
|
280
|
+
pat = self._path_pattern_cache.get(r.path)
|
|
281
|
+
if pat is None:
|
|
282
|
+
pat = self._compile_pattern(r.path)
|
|
283
|
+
self._path_pattern_cache[r.path] = pat
|
|
284
|
+
if pat.match(request_path):
|
|
285
|
+
return r
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
def _resolve_uri(self, route: Route) -> Optional[str]:
|
|
289
|
+
"""解析目标服务URI"""
|
|
290
|
+
if route.uri:
|
|
291
|
+
return route.uri.rstrip('/')
|
|
292
|
+
if route.service_id and self.discovery:
|
|
293
|
+
instances = self._get_instances(route.service_id)
|
|
294
|
+
if instances:
|
|
295
|
+
inst = LoadBalancerStrategy.round_robin(instances)
|
|
296
|
+
host = inst.get('ip') or inst.get('host', '127.0.0.1')
|
|
297
|
+
port = inst.get('port', 80)
|
|
298
|
+
scheme = inst.get('scheme', 'http')
|
|
299
|
+
return f"{scheme}://{host}:{port}"
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
def _get_instances(self, service_id: str) -> List[dict]:
|
|
303
|
+
"""获取服务实例列表"""
|
|
304
|
+
try:
|
|
305
|
+
instances = self.discovery.get_instances(service_id)
|
|
306
|
+
if instances:
|
|
307
|
+
return instances if isinstance(instances, list) else [instances]
|
|
308
|
+
except Exception as e:
|
|
309
|
+
logger.warning(f"[Gateway] Failed to discover {service_id}: {e}")
|
|
310
|
+
return []
|
|
311
|
+
|
|
312
|
+
def rewrite_path(self, route: Route, request_path: str) -> str:
|
|
313
|
+
"""路径重写"""
|
|
314
|
+
path = request_path
|
|
315
|
+
if route.strip_prefix:
|
|
316
|
+
# 去除匹配的前缀部分
|
|
317
|
+
parts = route.path.split('/**')[0].rstrip('*').rstrip('/')
|
|
318
|
+
if path.startswith(parts):
|
|
319
|
+
path = path[len(parts):] or '/'
|
|
320
|
+
if route.prefix:
|
|
321
|
+
path = route.prefix.rstrip('/') + '/' + path.lstrip('/')
|
|
322
|
+
return path
|
|
323
|
+
|
|
324
|
+
async def _run_filter(self, flt: GatewayFilter, method: str,
|
|
325
|
+
ctx: FilterContext) -> Any:
|
|
326
|
+
result = getattr(flt, method)(ctx)
|
|
327
|
+
if inspect.isawaitable(result):
|
|
328
|
+
return await result
|
|
329
|
+
return result
|
|
330
|
+
|
|
331
|
+
async def handle_asgi(self, request: Request) -> Response:
|
|
332
|
+
"""转发一个 Starlette/FastAPI 请求。"""
|
|
333
|
+
path = request.url.path
|
|
334
|
+
method = request.method
|
|
335
|
+
route = self.match_route(path)
|
|
336
|
+
if route is None:
|
|
337
|
+
return JSONResponse({"error": "No route matched", "path": path}, status_code=404)
|
|
338
|
+
|
|
339
|
+
headers = dict(request.headers)
|
|
340
|
+
query_items = list(request.query_params.multi_items())
|
|
341
|
+
query_dict = dict(query_items)
|
|
342
|
+
|
|
343
|
+
ctx = FilterContext(
|
|
344
|
+
route=route,
|
|
345
|
+
request_path=path,
|
|
346
|
+
request_headers=headers,
|
|
347
|
+
request_method=method,
|
|
348
|
+
request_query=query_dict,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# 执行前置过滤器
|
|
352
|
+
for flt in self.filters:
|
|
353
|
+
if not await self._run_filter(flt, 'pre_filter', ctx):
|
|
354
|
+
return JSONResponse(
|
|
355
|
+
{"error": "Gateway filter blocked", "details": ctx.response_headers},
|
|
356
|
+
status_code=ctx.response_status or 403,
|
|
357
|
+
headers=ctx.response_headers,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
target_uri = self._resolve_uri(route)
|
|
361
|
+
if not target_uri:
|
|
362
|
+
return JSONResponse(
|
|
363
|
+
{"error": "Service unavailable", "service": route.service_id},
|
|
364
|
+
status_code=503,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
forward_path = self.rewrite_path(route, path)
|
|
368
|
+
ctx.attributes['forward_uri'] = target_uri + forward_path
|
|
369
|
+
|
|
370
|
+
content_length = request.headers.get('content-length')
|
|
371
|
+
if self.max_body_size > 0 and content_length:
|
|
372
|
+
try:
|
|
373
|
+
if int(content_length) > self.max_body_size:
|
|
374
|
+
return JSONResponse({"error": "Request body too large"}, status_code=413)
|
|
375
|
+
except ValueError:
|
|
376
|
+
pass
|
|
377
|
+
|
|
378
|
+
if self.max_body_size > 0:
|
|
379
|
+
chunks = []
|
|
380
|
+
body_size = 0
|
|
381
|
+
async for chunk in request.stream():
|
|
382
|
+
body_size += len(chunk)
|
|
383
|
+
if body_size > self.max_body_size:
|
|
384
|
+
return JSONResponse({"error": "Request body too large"}, status_code=413)
|
|
385
|
+
chunks.append(chunk)
|
|
386
|
+
body = b''.join(chunks)
|
|
387
|
+
else:
|
|
388
|
+
body = await request.body()
|
|
389
|
+
|
|
390
|
+
request_headers = {
|
|
391
|
+
key: value for key, value in ctx.request_headers.items()
|
|
392
|
+
if key.lower() not in self._HOP_BY_HOP_HEADERS | {'host', 'content-length'}
|
|
393
|
+
}
|
|
394
|
+
try:
|
|
395
|
+
target_url = target_uri + forward_path
|
|
396
|
+
upstream = await self._get_client().request(
|
|
397
|
+
method,
|
|
398
|
+
target_url,
|
|
399
|
+
params=query_items,
|
|
400
|
+
headers=request_headers,
|
|
401
|
+
content=body,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
ctx.response_status = upstream.status_code
|
|
405
|
+
response_headers = {
|
|
406
|
+
key: value for key, value in upstream.headers.items()
|
|
407
|
+
if key.lower() not in self._HOP_BY_HOP_HEADERS | {'content-length'}
|
|
408
|
+
}
|
|
409
|
+
ctx.response_headers.update(response_headers)
|
|
410
|
+
|
|
411
|
+
# 执行后置过滤器
|
|
412
|
+
for flt in self.filters:
|
|
413
|
+
try:
|
|
414
|
+
await self._run_filter(flt, 'post_filter', ctx)
|
|
415
|
+
except Exception:
|
|
416
|
+
logger.exception("[Gateway] post filter failed")
|
|
417
|
+
|
|
418
|
+
return Response(
|
|
419
|
+
content=upstream.content,
|
|
420
|
+
status_code=upstream.status_code,
|
|
421
|
+
headers=ctx.response_headers,
|
|
422
|
+
)
|
|
423
|
+
except httpx.TimeoutException as exc:
|
|
424
|
+
logger.warning(f"[Gateway] Upstream timeout: {exc}")
|
|
425
|
+
return JSONResponse({"error": "Gateway timeout"}, status_code=504)
|
|
426
|
+
except httpx.HTTPError as exc:
|
|
427
|
+
logger.warning(f"[Gateway] Upstream request failed: {exc}")
|
|
428
|
+
return JSONResponse({"error": "Bad gateway"}, status_code=502)
|
|
429
|
+
except Exception:
|
|
430
|
+
logger.exception("[Gateway] Proxy error")
|
|
431
|
+
return JSONResponse({"error": "Gateway internal error"}, status_code=500)
|
|
432
|
+
|
|
433
|
+
def get_routes(self) -> List[Dict]:
|
|
434
|
+
"""获取所有路由信息"""
|
|
435
|
+
return [{
|
|
436
|
+
'id': r.id,
|
|
437
|
+
'path': r.path,
|
|
438
|
+
'service_id': r.service_id,
|
|
439
|
+
'uri': r.uri,
|
|
440
|
+
'enabled': r.enabled,
|
|
441
|
+
} for r in self.routes]
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# 全局网关实例
|
|
445
|
+
_gateway_instance: Optional[GatewayRouter] = None
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def get_gateway(discovery_client=None) -> GatewayRouter:
|
|
449
|
+
global _gateway_instance
|
|
450
|
+
if _gateway_instance is None:
|
|
451
|
+
_gateway_instance = GatewayRouter(discovery_client=discovery_client)
|
|
452
|
+
return _gateway_instance
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
负载均衡模块
|
|
3
|
+
提供多种负载均衡算法
|
|
4
|
+
"""
|
|
5
|
+
import random
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Dict, List, Any, Optional
|
|
8
|
+
from spring.cloud import discovery
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("Spring.Cloud.LoadBalancer")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LoadBalancer:
|
|
14
|
+
"""负载均衡器"""
|
|
15
|
+
|
|
16
|
+
_instance = None
|
|
17
|
+
_lock = __import__('threading').Lock()
|
|
18
|
+
|
|
19
|
+
def __new__(cls, *args, **kwargs):
|
|
20
|
+
if cls._instance is None:
|
|
21
|
+
with cls._lock:
|
|
22
|
+
if cls._instance is None:
|
|
23
|
+
cls._instance = super().__new__(cls)
|
|
24
|
+
return cls._instance
|
|
25
|
+
|
|
26
|
+
def __init__(self, strategy: str = "round_robin"):
|
|
27
|
+
if hasattr(self, '_initialized'):
|
|
28
|
+
return
|
|
29
|
+
self.strategy = strategy
|
|
30
|
+
self._round_robin_index: Dict[str, int] = {}
|
|
31
|
+
self._initialized = True
|
|
32
|
+
|
|
33
|
+
def get_instances(self, service_name: str) -> List[Dict[str, Any]]:
|
|
34
|
+
"""
|
|
35
|
+
获取服务实例列表
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
service_name: 服务名称
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
实例列表
|
|
42
|
+
"""
|
|
43
|
+
return discovery.nacos_client.get_service_instances(service_name)
|
|
44
|
+
|
|
45
|
+
def select_instance(self, instances: List[Dict[str, Any]],
|
|
46
|
+
strategy: str = None) -> Dict[str, Any]:
|
|
47
|
+
"""
|
|
48
|
+
根据负载均衡策略选择实例
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
instances: 实例列表
|
|
52
|
+
strategy: 策略名称(round_robin/random/weighted)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
选中的实例
|
|
56
|
+
"""
|
|
57
|
+
if not instances:
|
|
58
|
+
raise Exception("No instances available")
|
|
59
|
+
|
|
60
|
+
strategy = strategy or self.strategy
|
|
61
|
+
|
|
62
|
+
# 过滤健康实例
|
|
63
|
+
healthy_instances = [i for i in instances if i.get('healthy', True)]
|
|
64
|
+
if not healthy_instances:
|
|
65
|
+
# 如果没有健康实例,返回第一个实例
|
|
66
|
+
return instances[0]
|
|
67
|
+
|
|
68
|
+
if strategy == "round_robin":
|
|
69
|
+
return self._round_robin(healthy_instances)
|
|
70
|
+
elif strategy == "random":
|
|
71
|
+
return self._random(healthy_instances)
|
|
72
|
+
elif strategy == "weighted":
|
|
73
|
+
return self._weighted(healthy_instances)
|
|
74
|
+
else:
|
|
75
|
+
return self._round_robin(healthy_instances)
|
|
76
|
+
|
|
77
|
+
def _round_robin(self, instances: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
78
|
+
"""
|
|
79
|
+
轮询策略
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
instances: 实例列表
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
选中的实例
|
|
86
|
+
"""
|
|
87
|
+
# 生成一个唯一的key用于追踪索引
|
|
88
|
+
key = '|'.join(
|
|
89
|
+
sorted(f"{item.get('ip', '')}:{item.get('port', '')}" for item in instances)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if key not in self._round_robin_index:
|
|
93
|
+
self._round_robin_index[key] = 0
|
|
94
|
+
|
|
95
|
+
index = self._round_robin_index[key]
|
|
96
|
+
instance = instances[index % len(instances)]
|
|
97
|
+
self._round_robin_index[key] = index + 1
|
|
98
|
+
|
|
99
|
+
return instance
|
|
100
|
+
|
|
101
|
+
def _random(self, instances: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
102
|
+
"""
|
|
103
|
+
随机策略
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
instances: 实例列表
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
选中的实例
|
|
110
|
+
"""
|
|
111
|
+
return random.choice(instances)
|
|
112
|
+
|
|
113
|
+
def _weighted(self, instances: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
114
|
+
"""
|
|
115
|
+
加权随机策略
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
instances: 实例列表
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
选中的实例
|
|
122
|
+
"""
|
|
123
|
+
total_weight = sum(instance.get('weight', 1) for instance in instances)
|
|
124
|
+
|
|
125
|
+
if total_weight <= 0:
|
|
126
|
+
return random.choice(instances)
|
|
127
|
+
|
|
128
|
+
random_weight = random.uniform(0, total_weight)
|
|
129
|
+
|
|
130
|
+
current_weight = 0
|
|
131
|
+
for instance in instances:
|
|
132
|
+
current_weight += instance.get('weight', 1)
|
|
133
|
+
if current_weight >= random_weight:
|
|
134
|
+
return instance
|
|
135
|
+
|
|
136
|
+
return instances[-1]
|
|
137
|
+
|
|
138
|
+
def set_strategy(self, strategy: str):
|
|
139
|
+
"""
|
|
140
|
+
设置负载均衡策略
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
strategy: 策略名称
|
|
144
|
+
"""
|
|
145
|
+
self.strategy = strategy
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# 创建全局负载均衡器实例
|
|
149
|
+
load_balancer = LoadBalancer()
|