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,755 @@
1
+ from typing import Type, Any, Dict, Callable, List, Optional, get_args, get_origin, Union
2
+ import asyncio
3
+ import json
4
+ import logging
5
+ from datetime import datetime, date
6
+ from decimal import Decimal
7
+ from fastapi import FastAPI, Request, Response
8
+ from fastapi.routing import APIRoute
9
+ from fastapi.responses import JSONResponse
10
+ from starlette.concurrency import run_in_threadpool
11
+ from spring.context.application_context import ApplicationContext
12
+ from spring.annotations.core import (
13
+ RestController,
14
+ Controller,
15
+ RequestMapping,
16
+ GetMapping,
17
+ PostMapping,
18
+ PutMapping,
19
+ PatchMapping,
20
+ DeleteMapping,
21
+ RequestParam,
22
+ PathVariable,
23
+ RequestBody,
24
+ Valid,
25
+ Validated,
26
+ RequestHeader,
27
+ CookieValue,
28
+ CrossOrigin,
29
+ ControllerAdvice,
30
+ ExceptionHandler,
31
+ ResponseStatus,
32
+ )
33
+ from spring.web.result import Result
34
+ import os
35
+ import inspect
36
+ import re
37
+
38
+
39
+ class _JsonEncoder(json.JSONEncoder):
40
+ """扩展 JSON 编码器,支持 datetime、date、Decimal、bytes 等非原生类型"""
41
+ def default(self, obj):
42
+ if isinstance(obj, (datetime, date)):
43
+ return obj.isoformat()
44
+ if isinstance(obj, Decimal):
45
+ return float(obj)
46
+ if isinstance(obj, bytes):
47
+ return obj.decode('utf-8', errors='replace')
48
+ return super().default(obj)
49
+
50
+
51
+ class _SyncHandlerOverloaded(RuntimeError):
52
+ """Raised when the bounded synchronous-handler queue cannot accept work."""
53
+
54
+
55
+ class WebApplicationContext:
56
+ def __init__(self, application_context: ApplicationContext, static_dir: str = None,
57
+ interceptor_registry: Any = None):
58
+ self.application_context = application_context
59
+ # ---- Swagger/OpenAPI 配置 ----
60
+ from spring.web.swagger import SwaggerConfig
61
+ try:
62
+ config = application_context.get_config()
63
+ except (AttributeError, TypeError):
64
+ config = {}
65
+ self.swagger_config = SwaggerConfig.from_config(config)
66
+ self.fastapi_app = FastAPI(**self.swagger_config.to_fastapi_kwargs())
67
+ # 收集所有 Controller 类(用于 @Tag/@SecurityScheme 全局元数据)
68
+ self._controller_classes: List[Type] = []
69
+ # 收集方法级 @Parameter 元数据:{operation_id 或 path:method: [Parameter]}
70
+ self._method_param_meta: Dict[str, list] = {}
71
+ self._routes: List[APIRoute] = []
72
+ self._exception_handlers: Dict[Type[Exception], Callable] = {}
73
+ self._static_dir = static_dir
74
+ self._logger = logging.getLogger("Spring.Web")
75
+ self._interceptor_registry = interceptor_registry
76
+ self._interceptors_registered = False
77
+ thread_pool = self._get_thread_pool_config()
78
+ self._sync_max_workers = max(1, int(thread_pool.get('max_workers', 40)))
79
+ self._sync_max_queue = max(0, int(thread_pool.get('max_queue', 100)))
80
+ self._sync_queue_timeout = max(
81
+ 0.001, float(thread_pool.get('queue_timeout', 0.1))
82
+ )
83
+ self._sync_capacity = asyncio.Semaphore(
84
+ self._sync_max_workers + self._sync_max_queue
85
+ )
86
+
87
+ def init(self) -> None:
88
+ self.fastapi_app.router.add_event_handler(
89
+ 'startup', self._configure_sync_thread_pool
90
+ )
91
+ self._register_controllers()
92
+ self._register_interceptors()
93
+ self._register_exception_handlers()
94
+ self._register_cors_middleware()
95
+ self._register_static_files()
96
+ self._register_health_endpoints()
97
+ self._register_shutdown_handlers()
98
+ self._configure_swagger()
99
+
100
+ def _configure_swagger(self) -> None:
101
+ """在路由注册完成后,自定义 ``app.openapi()`` 注入全局 securitySchemes、
102
+ ``@Schema`` 模型描述与 ``@Parameter`` 参数描述。"""
103
+ from spring.web.swagger import (
104
+ configure_swagger, collect_security_schemes, register_schema, Schema,
105
+ )
106
+ # 收集全局 @SecurityScheme
107
+ security_schemes = collect_security_schemes(self._controller_classes)
108
+ # 注册 @Schema 标注的模型类
109
+ for cls in self._controller_classes:
110
+ for ann in (getattr(cls, '__spring_annotations__', []) or []):
111
+ if isinstance(ann, Schema) and getattr(ann, '_original_class', None):
112
+ register_schema(ann._original_class, ann)
113
+ configure_swagger(
114
+ self.fastapi_app,
115
+ self.swagger_config,
116
+ security_schemes=security_schemes,
117
+ method_param_meta=self._method_param_meta,
118
+ )
119
+
120
+ def _get_thread_pool_config(self) -> Dict[str, Any]:
121
+ try:
122
+ config = self.application_context.get_config()
123
+ except (AttributeError, TypeError):
124
+ return {}
125
+ server = config.get('server', {}) if isinstance(config, dict) else {}
126
+ value = server.get('thread_pool', server.get('thread-pool', {}))
127
+ return value if isinstance(value, dict) else {}
128
+
129
+ async def _configure_sync_thread_pool(self) -> None:
130
+ """Set AnyIO's per-worker thread limit after the ASGI loop starts."""
131
+ from anyio.to_thread import current_default_thread_limiter
132
+
133
+ current_default_thread_limiter().total_tokens = self._sync_max_workers
134
+
135
+ async def _run_sync_handler(self, handler: Callable, call_params: Dict[str, Any]) -> Any:
136
+ try:
137
+ await asyncio.wait_for(
138
+ self._sync_capacity.acquire(), timeout=self._sync_queue_timeout
139
+ )
140
+ except asyncio.TimeoutError as exc:
141
+ raise _SyncHandlerOverloaded(
142
+ "Synchronous request capacity exhausted"
143
+ ) from exc
144
+ try:
145
+ return await run_in_threadpool(handler, **call_params)
146
+ finally:
147
+ self._sync_capacity.release()
148
+
149
+ def _register_interceptors(self) -> None:
150
+ """Attach managed ``HandlerInterceptor`` beans to the HTTP lifecycle."""
151
+ if self._interceptors_registered:
152
+ return
153
+ from spring.web.interceptor import HandlerInterceptor, InterceptorManager, InterceptorRegistry
154
+
155
+ registry = self._interceptor_registry or InterceptorRegistry()
156
+ if self._interceptor_registry is None:
157
+ for bean_name in self.application_context.get_bean_names():
158
+ try:
159
+ bean = self.application_context.get_bean(bean_name)
160
+ except Exception:
161
+ continue
162
+ if isinstance(bean, HandlerInterceptor):
163
+ registry.add_interceptor(bean)
164
+ if not registry.get_interceptors():
165
+ self._interceptors_registered = True
166
+ return
167
+
168
+ manager = InterceptorManager(registry)
169
+
170
+ @self.fastapi_app.middleware("http")
171
+ async def interceptor_middleware(request: Request, call_next):
172
+ handler = request.scope.get("endpoint") or (lambda: None)
173
+ response = Response(status_code=500)
174
+ error = None
175
+ try:
176
+ if not await manager.apply_pre_handle(request, handler):
177
+ return Response(status_code=403, content="Request rejected by interceptor")
178
+ response = await call_next(request)
179
+ await manager.apply_post_handle(request, response, handler)
180
+ return response
181
+ except Exception as exc:
182
+ error = exc
183
+ raise
184
+ finally:
185
+ try:
186
+ await manager.apply_after_completion(
187
+ request, response, handler, error
188
+ )
189
+ except Exception:
190
+ self._logger.exception("Interceptor after_completion failed")
191
+
192
+ self._interceptors_registered = True
193
+
194
+ def _register_controllers(self) -> None:
195
+ self._logger.info(f"Registering controllers, found {len(self.application_context.get_bean_names())} beans")
196
+ for bean_name in self.application_context.get_bean_names():
197
+ definition = self.application_context.bean_factory.get_bean_definition(bean_name)
198
+ if not definition:
199
+ continue
200
+
201
+ annotations = definition.annotations
202
+ if RestController._annotation_type not in annotations and \
203
+ Controller._annotation_type not in annotations:
204
+ continue
205
+
206
+ self._logger.info(f"Found controller: {bean_name}")
207
+ controller_instance = self.application_context.get_bean(bean_name)
208
+ controller_class = controller_instance.__class__
209
+ self._controller_classes.append(controller_class)
210
+
211
+ class_mapping = self._get_class_mapping(controller_class)
212
+ class_path = class_mapping.get('path', '')
213
+ self._logger.info(f"Controller path: {class_path}")
214
+
215
+ # 按方法定义顺序注册路由(遍历 MRO 的 __dict__,Python 3.7+ 保留定义顺序)。
216
+ # 对齐 Spring MVC 静态路径优先的体验:开发者可将静态路径(如 /list)声明在
217
+ # 动态路径(如 /{user_id})之前,避免被动态路径拦截。
218
+ # 注意:不能用 inspect.getmembers(按字母序),否则 /{user_id} 会拦截 /list。
219
+ for method_name in self._iter_handler_names(controller_class):
220
+ method = getattr(controller_instance, method_name)
221
+ self._logger.info(f"Registering method: {method_name}")
222
+ self._register_handler(controller_instance, method.__func__, class_path)
223
+
224
+ @staticmethod
225
+ def _iter_handler_names(controller_class: Type):
226
+ """按定义顺序遍历 Controller 及其 MRO 上的 handler 方法名(跳过 `_` 开头)。
227
+
228
+ 遍历 ``__mro__`` 的 ``__dict__`` 以保留源码定义顺序(Python 3.7+ 类命名空间有序),
229
+ 同时覆盖继承的 handler;子类同名方法覆盖父类。
230
+ """
231
+ seen = set()
232
+ for klass in controller_class.__mro__:
233
+ for name, member in vars(klass).items():
234
+ if name.startswith('_') or name in seen:
235
+ continue
236
+ if inspect.isfunction(member) or inspect.ismethod(member):
237
+ seen.add(name)
238
+ yield name
239
+
240
+ def _get_class_mapping(self, controller_class: Type) -> Dict[str, Any]:
241
+ annotations = getattr(controller_class, '__spring_annotations__', [])
242
+ for annotation in annotations:
243
+ if isinstance(annotation, RequestMapping):
244
+ return {
245
+ 'path': annotation.path,
246
+ 'method': annotation.method,
247
+ 'consumes': annotation.consumes,
248
+ 'produces': annotation.produces,
249
+ }
250
+ return {'path': '', 'method': [], 'consumes': None, 'produces': None}
251
+
252
+ def _register_handler(self, controller_instance: Any, method: Callable, class_path: str) -> None:
253
+ annotations = getattr(method, '__spring_annotations__', [])
254
+ if not annotations:
255
+ return
256
+
257
+ # 收集 Swagger/OpenAPI 注解元数据(@Operation/@ApiResponse/@SecurityRequirement)
258
+ from spring.web.swagger import collect_openapi_metadata, Parameter
259
+ controller_class = controller_instance.__class__
260
+ openapi_meta = collect_openapi_metadata(method, controller_class)
261
+ # 收集方法级 @Parameter 元数据,供 configure_swagger 后处理注入
262
+ method_params = [a for a in (getattr(method, '__spring_annotations__', []) or []) if isinstance(a, Parameter)]
263
+
264
+ for annotation in annotations:
265
+ if isinstance(annotation, (RequestMapping, GetMapping, PostMapping, PutMapping, PatchMapping, DeleteMapping)):
266
+ paths = annotation.path if isinstance(annotation.path, list) else [annotation.path]
267
+ class_paths = class_path if isinstance(class_path, list) else [class_path]
268
+ methods = annotation.method or ['GET']
269
+ for raw_path in paths or ['']:
270
+ path = raw_path or '/' + method.__name__
271
+ prefix = class_paths[0] if class_paths else ''
272
+ if prefix:
273
+ path = prefix.rstrip('/') + '/' + path.lstrip('/')
274
+ endpoint = self._create_endpoint(controller_instance, method, path)
275
+ for http_method in methods:
276
+ self._add_route(http_method.lower(), path, endpoint, openapi_meta)
277
+ # 记录 @Parameter 元数据(key = path:method,与后处理一致)
278
+ if method_params:
279
+ key = f"{path}:{http_method.lower()}"
280
+ self._method_param_meta[key] = method_params
281
+
282
+ def _create_endpoint(self, controller_instance: Any, method: Callable, path: str) -> Callable:
283
+ from fastapi import Path as FastPath, Query as FastQuery, Body as FastBody
284
+
285
+ sig = inspect.signature(method)
286
+ param_infos = []
287
+
288
+ for param_name, param in sig.parameters.items():
289
+ if param_name == 'self':
290
+ continue
291
+
292
+ # 检查是否在路径中
293
+ path_param_match = re.search(r'\{' + param_name + r'\}', path)
294
+
295
+ if isinstance(param.default, PathVariable):
296
+ ann = param.default
297
+ actual_name = ann.name or param_name
298
+ param_infos.append({
299
+ 'name': param_name, 'kind': 'path', 'http_name': actual_name,
300
+ 'annotation': param.annotation if param.annotation is not inspect.Parameter.empty else str,
301
+ 'default': None, 'required': True,
302
+ })
303
+ elif isinstance(param.default, RequestParam):
304
+ ann = param.default
305
+ actual_name = ann.name or param_name
306
+ param_infos.append({
307
+ 'name': param_name, 'kind': 'query', 'http_name': actual_name,
308
+ 'annotation': param.annotation if param.annotation is not inspect.Parameter.empty else str,
309
+ 'default': ann.default, 'required': ann.required,
310
+ })
311
+ elif isinstance(param.default, (RequestBody, Valid, Validated)):
312
+ body_annotation = (
313
+ param.annotation
314
+ if param.annotation is not inspect.Parameter.empty
315
+ else dict
316
+ )
317
+ param_infos.append({
318
+ 'name': param_name, 'kind': 'body', 'http_name': param_name,
319
+ 'annotation': body_annotation, 'default': None,
320
+ 'required': getattr(param.default, 'required', True),
321
+ })
322
+ elif isinstance(param.default, RequestHeader):
323
+ ann = param.default
324
+ header_name = ann.name or param_name.replace('_', '-')
325
+ param_infos.append({
326
+ 'name': param_name, 'kind': 'header', 'http_name': header_name,
327
+ 'annotation': param.annotation if param.annotation is not inspect.Parameter.empty else str,
328
+ 'default': ann.default, 'required': ann.required,
329
+ })
330
+ elif isinstance(param.default, CookieValue):
331
+ ann = param.default
332
+ cookie_name = ann.name or param_name
333
+ param_infos.append({
334
+ 'name': param_name, 'kind': 'cookie', 'http_name': cookie_name,
335
+ 'annotation': param.annotation if param.annotation is not inspect.Parameter.empty else str,
336
+ 'default': ann.default, 'required': ann.required,
337
+ })
338
+ elif path_param_match:
339
+ # 路径中包含该参数名,视为路径参数
340
+ param_infos.append({
341
+ 'name': param_name, 'kind': 'path', 'http_name': param_name,
342
+ 'annotation': param.annotation if param.annotation is not inspect.Parameter.empty else str,
343
+ 'default': None, 'required': True,
344
+ })
345
+ elif param.annotation == dict:
346
+ # dict 类型的参数,视为请求体参数
347
+ param_infos.append({
348
+ 'name': param_name, 'kind': 'body', 'http_name': param_name,
349
+ 'annotation': dict, 'default': None, 'required': param.default is inspect.Parameter.empty,
350
+ })
351
+ elif param.default is not inspect.Parameter.empty:
352
+ # 有默认值的参数,视为查询参数
353
+ param_infos.append({
354
+ 'name': param_name, 'kind': 'query', 'http_name': param_name,
355
+ 'annotation': param.annotation if param.annotation is not inspect.Parameter.empty else str,
356
+ 'default': param.default, 'required': False,
357
+ })
358
+ else:
359
+ # 无默认值且不在路径中的参数,视为必需查询参数
360
+ param_infos.append({
361
+ 'name': param_name, 'kind': 'query', 'http_name': param_name,
362
+ 'annotation': param.annotation if param.annotation is not inspect.Parameter.empty else str,
363
+ 'default': None, 'required': True,
364
+ })
365
+
366
+ # 为动态 endpoint 构建参数签名
367
+ endpoint_params = [
368
+ inspect.Parameter('request', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Request),
369
+ ]
370
+ for info in param_infos:
371
+ if info['kind'] == 'path':
372
+ endpoint_params.append(inspect.Parameter(
373
+ info['name'], inspect.Parameter.POSITIONAL_OR_KEYWORD,
374
+ default=FastPath(...), annotation=info['annotation'],
375
+ ))
376
+ elif info['kind'] == 'query':
377
+ alias = info['http_name'] if info['http_name'] != info['name'] else None
378
+ if info['required'] and info['default'] is None:
379
+ endpoint_params.append(inspect.Parameter(
380
+ info['name'], inspect.Parameter.POSITIONAL_OR_KEYWORD,
381
+ default=FastQuery(..., alias=alias), annotation=Optional[info['annotation']],
382
+ ))
383
+ else:
384
+ endpoint_params.append(inspect.Parameter(
385
+ info['name'], inspect.Parameter.POSITIONAL_OR_KEYWORD,
386
+ default=FastQuery(info['default'], alias=alias), annotation=Optional[info['annotation']],
387
+ ))
388
+ elif info['kind'] == 'body':
389
+ endpoint_params.append(inspect.Parameter(
390
+ info['name'], inspect.Parameter.POSITIONAL_OR_KEYWORD,
391
+ default=FastBody(... if info['required'] else None), annotation=info['annotation'],
392
+ ))
393
+
394
+ def make_endpoint(controller_instance, method, param_infos):
395
+ method_annotations = getattr(method, '__spring_annotations__', [])
396
+ controller_annotations = getattr(
397
+ controller_instance.__class__, '__spring_annotations__', []
398
+ )
399
+ response_status = next(
400
+ (item for item in method_annotations if isinstance(item, ResponseStatus)),
401
+ next(
402
+ (
403
+ item for item in controller_annotations
404
+ if isinstance(item, ResponseStatus)
405
+ ),
406
+ None,
407
+ ),
408
+ )
409
+ requires_authentication = any(
410
+ type(item).__name__ == 'Authenticate' for item in method_annotations
411
+ )
412
+
413
+ async def endpoint(request: Request, **kwargs):
414
+ try:
415
+ call_params = {}
416
+ for info in param_infos:
417
+ name = info['name']
418
+ if name in kwargs:
419
+ if kwargs[name] is None and info['required'] and info['kind'] in {'query', 'path', 'body'}:
420
+ raise ValueError(f"请求参数 '{info['http_name']}' 不能为空")
421
+ call_params[name] = self._convert_type(kwargs[name], info['annotation'])
422
+ elif info['kind'] == 'body':
423
+ try:
424
+ body = await request.json()
425
+ except Exception:
426
+ if info['required']:
427
+ raise ValueError(f"Request body '{name}' is required")
428
+ body = None
429
+ call_params[name] = self._convert_type(body, info['annotation'])
430
+ elif info['kind'] == 'header':
431
+ value = request.headers.get(info['http_name'])
432
+ if value is None:
433
+ if info['required']:
434
+ raise ValueError(f"Header '{info['http_name']}' is required")
435
+ value = info['default']
436
+ call_params[name] = self._convert_type(value, info['annotation'])
437
+ elif info['kind'] == 'query':
438
+ if info['required']:
439
+ raise ValueError(f"Query parameter '{info['http_name']}' is required")
440
+ call_params[name] = info['default']
441
+ elif info['kind'] == 'cookie':
442
+ value = request.cookies.get(info['http_name'])
443
+ if value is None:
444
+ if info['required']:
445
+ raise ValueError(f"Cookie '{info['http_name']}' is required")
446
+ value = info['default']
447
+ call_params[name] = self._convert_type(value, info['annotation'])
448
+
449
+ if requires_authentication:
450
+ call_params['_spring_request'] = request
451
+ handler = getattr(controller_instance, method.__name__)
452
+ if inspect.iscoroutinefunction(handler):
453
+ result = await handler(**call_params)
454
+ else:
455
+ # FastAPI normally offloads sync endpoints automatically. SpringBootAI
456
+ # wraps every controller in an async adapter, so it must preserve
457
+ # that behavior explicitly for blocking DB/HTTP/AI workloads.
458
+ result = await self._run_sync_handler(handler, call_params)
459
+ if inspect.isawaitable(result):
460
+ result = await result
461
+
462
+ if not isinstance(result, Result):
463
+ result = Result.success(data=result)
464
+ if response_status is not None:
465
+ result = Result(
466
+ code=response_status.code,
467
+ message=response_status.reason or result.message,
468
+ data=result.data,
469
+ )
470
+ return self._result_response(result)
471
+
472
+ except _SyncHandlerOverloaded as e:
473
+ return JSONResponse(
474
+ status_code=503,
475
+ headers={'Retry-After': '1'},
476
+ content={
477
+ 'code': 503,
478
+ 'message': str(e),
479
+ 'data': None,
480
+ },
481
+ )
482
+ except Exception as e:
483
+ # 记录详细错误日志
484
+ import traceback
485
+ self._logger.error(f"Request processing error: {str(e)}")
486
+ self._logger.error(traceback.format_exc())
487
+
488
+ handler = self._find_exception_handler(e)
489
+ if handler is not None:
490
+ handler_result = handler(e)
491
+ if inspect.iscoroutine(handler_result):
492
+ handler_result = await handler_result
493
+ if isinstance(handler_result, Result):
494
+ return self._result_response(handler_result)
495
+ return self._result_response(
496
+ Result.error(message="Internal server error", code=500)
497
+ )
498
+ status_code = getattr(e, 'status_code', None)
499
+ if status_code == 401:
500
+ return self._result_response(Result.unauthorized(message=str(e)))
501
+ if status_code == 403:
502
+ return self._result_response(Result.forbidden(message=str(e)))
503
+ if isinstance(e, (ValueError, TypeError)):
504
+ return self._result_response(Result.bad_request(message=str(e)))
505
+ # 生产环境隐藏详细错误信息
506
+ return self._result_response(
507
+ Result.error(message="Internal server error", code=500)
508
+ )
509
+
510
+ # 替换签名,让 FastAPI 正确识别路径/查询/body参数
511
+ original_sig = inspect.signature(endpoint)
512
+ new_sig = original_sig.replace(parameters=endpoint_params)
513
+ endpoint.__signature__ = new_sig
514
+ endpoint.__name__ = method.__name__
515
+ return endpoint
516
+
517
+ return make_endpoint(controller_instance, method, param_infos)
518
+
519
+ @staticmethod
520
+ def _result_response(result: Result) -> JSONResponse:
521
+ status_code = result.code if 100 <= result.code <= 599 else 500
522
+ return JSONResponse(
523
+ status_code=status_code,
524
+ content=json.loads(json.dumps({
525
+ 'code': result.code,
526
+ 'message': result.message,
527
+ 'data': result.data,
528
+ }, cls=_JsonEncoder)),
529
+ )
530
+
531
+ def _extract_path_param_names(self, path: str) -> List[str]:
532
+ return re.findall(r'\{([^}]+)\}', path)
533
+
534
+ def _find_exception_handler(self, error: Exception) -> Optional[Callable]:
535
+ """Match handlers using normal Python subclass semantics."""
536
+ candidates = [
537
+ (exception_type, handler)
538
+ for exception_type, handler in self._exception_handlers.items()
539
+ if isinstance(error, exception_type)
540
+ ]
541
+ if not candidates:
542
+ return None
543
+ candidates.sort(key=lambda item: len(getattr(item[0], '__mro__', ())), reverse=True)
544
+ return candidates[0][1]
545
+
546
+ def _convert_type(self, value: Any, target_type: Type) -> Any:
547
+ if value is None:
548
+ return None
549
+
550
+ origin = get_origin(target_type)
551
+ if origin is Union:
552
+ candidates = [item for item in get_args(target_type) if item is not type(None)]
553
+ if len(candidates) == 1:
554
+ return self._convert_type(value, candidates[0])
555
+
556
+ if target_type is int:
557
+ return int(value)
558
+ elif target_type is float:
559
+ return float(value)
560
+ elif target_type is bool:
561
+ return str(value).lower() == 'true'
562
+ elif target_type is str:
563
+ return str(value)
564
+ return value
565
+
566
+ def _add_route(self, http_method: str, path: str, endpoint: Callable,
567
+ openapi_meta: Optional[Dict[str, Any]] = None) -> None:
568
+ # 将 Swagger 注解元数据传给 FastAPI 路由装饰器(tags/summary/description/
569
+ # operation_id/deprecated/responses/security)
570
+ kwargs = dict(openapi_meta) if openapi_meta else {}
571
+ if http_method == 'get':
572
+ self.fastapi_app.get(path, **kwargs)(endpoint)
573
+ elif http_method == 'post':
574
+ self.fastapi_app.post(path, **kwargs)(endpoint)
575
+ elif http_method == 'put':
576
+ self.fastapi_app.put(path, **kwargs)(endpoint)
577
+ elif http_method == 'patch':
578
+ self.fastapi_app.patch(path, **kwargs)(endpoint)
579
+ elif http_method == 'delete':
580
+ self.fastapi_app.delete(path, **kwargs)(endpoint)
581
+
582
+ def _register_exception_handlers(self) -> None:
583
+ for bean_name in self.application_context.get_bean_names():
584
+ definition = self.application_context.bean_factory.get_bean_definition(bean_name)
585
+ if not definition:
586
+ continue
587
+
588
+ if ControllerAdvice._annotation_type in definition.annotations:
589
+ advice_instance = self.application_context.get_bean(bean_name)
590
+ advice_class = advice_instance.__class__
591
+
592
+ for method_name, method in inspect.getmembers(advice_class):
593
+ if not method_name.startswith('_') and inspect.isfunction(method):
594
+ annotations = getattr(method, '__spring_annotations__', [])
595
+ for annotation in annotations:
596
+ if isinstance(annotation, ExceptionHandler):
597
+ for exception_type in annotation.exceptions:
598
+ self._exception_handlers[exception_type] = method.__get__(advice_instance)
599
+
600
+ def _register_cors_middleware(self) -> None:
601
+ from fastapi.middleware.cors import CORSMiddleware
602
+
603
+ configured_cors = self.application_context.get_value('server.cors', {}) or {}
604
+ cors_config = {
605
+ "allow_origins": configured_cors.get('allow_origins', []),
606
+ "allow_credentials": configured_cors.get('allow_credentials', False),
607
+ "allow_methods": configured_cors.get(
608
+ 'allow_methods', ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
609
+ ),
610
+ "allow_headers": configured_cors.get(
611
+ 'allow_headers', ["Content-Type", "Authorization"]
612
+ ),
613
+ "max_age": configured_cors.get('max_age', 600),
614
+ }
615
+
616
+ for bean_name in self.application_context.get_bean_names():
617
+ definition = self.application_context.bean_factory.get_bean_definition(bean_name)
618
+ if not definition:
619
+ continue
620
+
621
+ if CrossOrigin._annotation_type in definition.annotations:
622
+ cors_annotations = definition.annotations[CrossOrigin._annotation_type]
623
+ if cors_annotations:
624
+ try:
625
+ cors_annotation = cors_annotations[0]
626
+ credentials = cors_annotation.allowCredentials
627
+ origins = cors_annotation.origins
628
+
629
+ if credentials and "*" in origins:
630
+ raise ValueError("CORS开启凭证时不能允许通配来源")
631
+
632
+ cors_config.update({
633
+ "allow_origins": origins,
634
+ "allow_methods": cors_annotation.methods,
635
+ "allow_headers": cors_annotation.allowedHeaders,
636
+ "allow_credentials": credentials,
637
+ "max_age": cors_annotation.maxAge,
638
+ })
639
+ except Exception as e:
640
+ self._logger.error(f"Failed to parse CORS configuration: {str(e)}")
641
+
642
+ break
643
+
644
+ self.fastapi_app.add_middleware(CORSMiddleware, **cors_config)
645
+
646
+ def _register_static_files(self) -> None:
647
+ """注册静态文件服务"""
648
+ if self._static_dir and os.path.isdir(self._static_dir):
649
+ from fastapi.staticfiles import StaticFiles
650
+ from fastapi.responses import FileResponse
651
+ import os
652
+
653
+ # 获取静态目录的绝对路径,用于路径安全验证
654
+ self._static_dir_abs = os.path.realpath(self._static_dir)
655
+
656
+ # 挂载静态文件目录
657
+ self.fastapi_app.mount("/static", StaticFiles(directory=self._static_dir), name="static")
658
+
659
+ # 添加首页路由
660
+ @self.fastapi_app.get("/")
661
+ async def serve_index():
662
+ index_path = os.path.join(self._static_dir_abs, "index.html")
663
+ if os.path.exists(index_path) and os.path.isfile(index_path):
664
+ return FileResponse(index_path)
665
+ return {"error": "index.html not found"}
666
+
667
+ # 添加其他静态文件路由(处理 js、css、images 等)
668
+ @self.fastapi_app.get("/{full_path:path}")
669
+ async def serve_static(full_path: str):
670
+ # 安全验证:防止路径遍历攻击
671
+ if '..' in full_path:
672
+ from fastapi import HTTPException
673
+ raise HTTPException(status_code=403, detail="Forbidden")
674
+
675
+ file_path = os.path.join(self._static_dir_abs, full_path)
676
+ # 使用 realpath 验证路径是否在静态目录内
677
+ real_file_path = os.path.realpath(file_path)
678
+
679
+ # 确保请求的文件在静态目录内
680
+ if not real_file_path.startswith(self._static_dir_abs + os.sep) and real_file_path != self._static_dir_abs:
681
+ from fastapi import HTTPException
682
+ raise HTTPException(status_code=403, detail="Forbidden")
683
+
684
+ if os.path.exists(real_file_path) and os.path.isfile(real_file_path):
685
+ return FileResponse(real_file_path)
686
+ # 如果是 API 请求,返回 404 让 API 路由处理
687
+ from fastapi import HTTPException
688
+ raise HTTPException(status_code=404, detail="Not Found")
689
+
690
+ print(f"静态文件服务已注册: {self._static_dir}")
691
+ else:
692
+ if self._static_dir:
693
+ print(f"警告: 静态文件目录不存在: {self._static_dir}")
694
+
695
+ def get_app(self) -> FastAPI:
696
+ return self.fastapi_app
697
+
698
+ def _register_health_endpoints(self) -> None:
699
+ """注册健康检查端点 + Actuator 运维端点。"""
700
+ try:
701
+ from spring.web.health import configure_health_checks, health_router
702
+ configure_health_checks(self.application_context)
703
+ self.fastapi_app.include_router(health_router, prefix="/actuator")
704
+ self._logger.info("Health check endpoints registered")
705
+ except Exception as e:
706
+ self._logger.warning(f"Failed to register health check endpoints: {e}")
707
+ # Actuator 标准运维端点(/env /loggers /metrics /beans /configprops /mappings /threaddump)
708
+ try:
709
+ from spring.web.actuator import actuator_router, configure_actuator
710
+ configure_actuator(self.application_context)
711
+ self.fastapi_app.include_router(actuator_router, prefix="/actuator")
712
+ self._logger.info("Actuator endpoints registered")
713
+ except Exception as e:
714
+ self._logger.warning(f"Failed to register actuator endpoints: {e}")
715
+
716
+ def _register_shutdown_handlers(self) -> None:
717
+ def close_resources() -> None:
718
+ try:
719
+ session_factory = self.application_context.get_bean('sqlSessionFactory')
720
+ except Exception:
721
+ session_factory = None
722
+ if session_factory is not None:
723
+ close = getattr(session_factory, 'close', None)
724
+ if callable(close):
725
+ close()
726
+
727
+ try:
728
+ from spring.messaging.rabbitmq import rabbitmq_client
729
+ rabbitmq_client.close()
730
+ except ImportError:
731
+ pass
732
+
733
+ try:
734
+ from spring.cloud.feign import FeignClientFactory
735
+ FeignClientFactory.close_all()
736
+ except ImportError:
737
+ pass
738
+
739
+ self.application_context.bean_factory.destroy_all()
740
+
741
+ self.fastapi_app.router.add_event_handler('shutdown', close_resources)
742
+
743
+ def run(self, host: str = "0.0.0.0", port: int = 8080, **kwargs) -> None:
744
+ # 优先使用 uvicorn,fallback 到其他方案
745
+ try:
746
+ import uvicorn
747
+ uvicorn.run(self.fastapi_app, host=host, port=port)
748
+ except ImportError:
749
+ try:
750
+ from a2wsgi import ASGIMiddleware, WSGIServer
751
+ wsgi_app = ASGIMiddleware(self.fastapi_app)
752
+ server = WSGIServer(wsgi_app, host=host, port=port)
753
+ server.run()
754
+ except ImportError:
755
+ raise RuntimeError("Neither uvicorn nor a2wsgi is installed. Please install one of them.")