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.
Files changed (175) hide show
  1. spring/__init__.py +66 -0
  2. spring/ai/__init__.py +78 -0
  3. spring/ai/advisors.py +139 -0
  4. spring/ai/annotations.py +74 -0
  5. spring/ai/autoconfig.py +481 -0
  6. spring/ai/core.py +391 -0
  7. spring/ai/etl.py +188 -0
  8. spring/ai/memory.py +109 -0
  9. spring/ai/observability.py +129 -0
  10. spring/ai/providers.py +789 -0
  11. spring/ai/resilience.py +258 -0
  12. spring/ai/tools.py +106 -0
  13. spring/ai/vectorstore.py +303 -0
  14. spring/annotations/__init__.py +188 -0
  15. spring/annotations/cache.py +126 -0
  16. spring/annotations/cloud.py +207 -0
  17. spring/annotations/conditional.py +272 -0
  18. spring/annotations/core.py +864 -0
  19. spring/annotations/messaging.py +107 -0
  20. spring/aop/__init__.py +4 -0
  21. spring/aop/cloud_aop.py +404 -0
  22. spring/aop/comprehensive_aop.py +1015 -0
  23. spring/aop/method_interceptor.py +19 -0
  24. spring/aop/proxy_factory.py +55 -0
  25. spring/cloud/__init__.py +76 -0
  26. spring/cloud/discovery.py +364 -0
  27. spring/cloud/feign.py +469 -0
  28. spring/cloud/gateway.py +452 -0
  29. spring/cloud/load_balancer.py +149 -0
  30. spring/cloud/seata.py +557 -0
  31. spring/cloud/sentinel.py +525 -0
  32. spring/cloud/tracer.py +337 -0
  33. spring/config/__init__.py +21 -0
  34. spring/config/binding.py +206 -0
  35. spring/config/config_loader.py +405 -0
  36. spring/context/__init__.py +13 -0
  37. spring/context/application_context.py +589 -0
  38. spring/context/bean_definition.py +70 -0
  39. spring/context/bean_factory.py +1052 -0
  40. spring/context/registry.py +58 -0
  41. spring/context/scanner.py +106 -0
  42. spring/core/__init__.py +3 -0
  43. spring/core/graceful_shutdown.py +196 -0
  44. spring/core/typing_utils.py +50 -0
  45. spring/csv/__init__.py +52 -0
  46. spring/csv/annotations.py +402 -0
  47. spring/csv/converters.py +69 -0
  48. spring/csv/easy_csv.py +95 -0
  49. spring/csv/exceptions.py +27 -0
  50. spring/csv/reader.py +195 -0
  51. spring/csv/writer.py +155 -0
  52. spring/data/__init__.py +54 -0
  53. spring/data/page.py +181 -0
  54. spring/data/repository.py +274 -0
  55. spring/data/specification.py +228 -0
  56. spring/datasource/__init__.py +66 -0
  57. spring/datasource/annotations.py +133 -0
  58. spring/datasource/context.py +69 -0
  59. spring/datasource/dynamic.py +148 -0
  60. spring/event/__init__.py +7 -0
  61. spring/event/publisher.py +69 -0
  62. spring/excel/__init__.py +51 -0
  63. spring/excel/annotations.py +405 -0
  64. spring/excel/converters.py +231 -0
  65. spring/excel/easy_excel.py +94 -0
  66. spring/excel/exceptions.py +31 -0
  67. spring/excel/reader.py +254 -0
  68. spring/excel/style.py +95 -0
  69. spring/excel/writer.py +197 -0
  70. spring/i18n/__init__.py +97 -0
  71. spring/i18n/accessor.py +94 -0
  72. spring/i18n/auto_config.py +177 -0
  73. spring/i18n/holder.py +106 -0
  74. spring/i18n/locale.py +152 -0
  75. spring/i18n/locale_resolver.py +367 -0
  76. spring/i18n/message_source.py +250 -0
  77. spring/i18n/middleware.py +79 -0
  78. spring/i18n/properties.py +168 -0
  79. spring/i18n/sources.py +255 -0
  80. spring/logging/__init__.py +1 -0
  81. spring/logging/loguru_logger.py +228 -0
  82. spring/main.py +378 -0
  83. spring/messaging/__init__.py +1 -0
  84. spring/messaging/rabbitmq.py +302 -0
  85. spring/monitoring/__init__.py +1 -0
  86. spring/monitoring/prometheus.py +199 -0
  87. spring/orm/__init__.py +258 -0
  88. spring/orm/database.py +222 -0
  89. spring/orm/ddl_auto.py +1217 -0
  90. spring/orm/migration.py +419 -0
  91. spring/orm/mybatis_integration.py +400 -0
  92. spring/orm/pymybatis/__init__.py +86 -0
  93. spring/orm/pymybatis/annotations/__init__.py +30 -0
  94. spring/orm/pymybatis/annotations/annotations.py +332 -0
  95. spring/orm/pymybatis/cache/__init__.py +47 -0
  96. spring/orm/pymybatis/cache/cache.py +371 -0
  97. spring/orm/pymybatis/cache/redis_cache.py +434 -0
  98. spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
  99. spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
  100. spring/orm/pymybatis/configuration.py +525 -0
  101. spring/orm/pymybatis/core/__init__.py +10 -0
  102. spring/orm/pymybatis/core/sql_session.py +1382 -0
  103. spring/orm/pymybatis/core/sql_session_factory.py +76 -0
  104. spring/orm/pymybatis/dialect/__init__.py +9 -0
  105. spring/orm/pymybatis/dialect/dialect.py +445 -0
  106. spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
  107. spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
  108. spring/orm/pymybatis/interceptor/__init__.py +31 -0
  109. spring/orm/pymybatis/interceptor/interceptor.py +427 -0
  110. spring/orm/pymybatis/mapper/__init__.py +9 -0
  111. spring/orm/pymybatis/mapper/mapper.py +540 -0
  112. spring/orm/pymybatis/metrics/__init__.py +41 -0
  113. spring/orm/pymybatis/metrics/metrics.py +595 -0
  114. spring/orm/pymybatis/pool/__init__.py +9 -0
  115. spring/orm/pymybatis/pool/connection_pool.py +711 -0
  116. spring/orm/pymybatis/security/__init__.py +19 -0
  117. spring/orm/pymybatis/security/access_control.py +415 -0
  118. spring/orm/pymybatis/security/password_encoder.py +293 -0
  119. spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
  120. spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
  121. spring/orm/pymybatis/transaction/__init__.py +9 -0
  122. spring/orm/pymybatis/transaction/transaction.py +288 -0
  123. spring/orm/pymybatis/type_handler/__init__.py +37 -0
  124. spring/orm/pymybatis/type_handler/type_handler.py +473 -0
  125. spring/orm/pymybatis/version.py +9 -0
  126. spring/orm/pymybatis/xml_parser/__init__.py +9 -0
  127. spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
  128. spring/retry/__init__.py +12 -0
  129. spring/retry/retry_annotations.py +71 -0
  130. spring/retry/retry_decorator.py +155 -0
  131. spring/scheduling/__init__.py +3 -0
  132. spring/scheduling/scheduler.py +389 -0
  133. spring/security/__init__.py +39 -0
  134. spring/security/jwt_utils.py +281 -0
  135. spring/security/replay_protection.py +206 -0
  136. spring/security/secret_manager.py +226 -0
  137. spring/security/security_aop.py +248 -0
  138. spring/security/security_context.py +172 -0
  139. spring/test/__init__.py +45 -0
  140. spring/test/slicing.py +341 -0
  141. spring/tracing/__init__.py +11 -0
  142. spring/tracing/skywalking.py +229 -0
  143. spring/tx/__init__.py +52 -0
  144. spring/tx/events.py +172 -0
  145. spring/tx/synchronization.py +143 -0
  146. spring/utils/__init__.py +5 -0
  147. spring/utils/banner.py +32 -0
  148. spring/utils/logger.py +73 -0
  149. spring/utils/redis_client.py +526 -0
  150. spring/validation/__init__.py +55 -0
  151. spring/validation/aop.py +141 -0
  152. spring/validation/constraints.py +357 -0
  153. spring/validation/exceptions.py +55 -0
  154. spring/validation/validator.py +139 -0
  155. spring/web/__init__.py +12 -0
  156. spring/web/actuator.py +319 -0
  157. spring/web/exception_handler.py +61 -0
  158. spring/web/health.py +399 -0
  159. spring/web/interceptor.py +91 -0
  160. spring/web/result.py +44 -0
  161. spring/web/swagger.py +601 -0
  162. spring/web/web_context.py +755 -0
  163. spring/websocket/__init__.py +86 -0
  164. spring/websocket/annotations.py +169 -0
  165. spring/websocket/broker.py +238 -0
  166. spring/websocket/exceptions.py +26 -0
  167. spring/websocket/handler.py +243 -0
  168. spring/websocket/router.py +526 -0
  169. spring/websocket/session.py +216 -0
  170. springbootai-1.8.0.dist-info/METADATA +2796 -0
  171. springbootai-1.8.0.dist-info/RECORD +175 -0
  172. springbootai-1.8.0.dist-info/WHEEL +5 -0
  173. springbootai-1.8.0.dist-info/entry_points.txt +2 -0
  174. springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
  175. springbootai-1.8.0.dist-info/top_level.txt +1 -0
@@ -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()