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
|
@@ -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.")
|