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
spring/main.py ADDED
@@ -0,0 +1,378 @@
1
+ from typing import Type, Optional
2
+ import socket
3
+ import signal
4
+ from spring.context.application_context import ApplicationContext
5
+ from spring.web.web_context import WebApplicationContext
6
+ from spring.utils.banner import BannerPrinter
7
+ from spring.utils.logger import SpringLogger
8
+ from spring.logging.loguru_logger import init_logging
9
+ from spring.orm.mybatis_integration import init_mybatis
10
+ from spring.annotations.cloud import EnableDiscoveryClient
11
+
12
+
13
+ class SpringApplication:
14
+ def __init__(self, main_class: Type):
15
+ self.main_class = main_class
16
+ self.application_context: Optional[ApplicationContext] = None
17
+ self.web_context: Optional[WebApplicationContext] = None
18
+ self.logger = SpringLogger()
19
+ self._discovery_registered = False
20
+ self._background_started = False
21
+
22
+ def run(self, **kwargs) -> None:
23
+ banner = BannerPrinter()
24
+ banner.print_banner()
25
+
26
+ try:
27
+ self._prepare_context()
28
+ self._start_web_server(**kwargs)
29
+ except Exception as e:
30
+ self.logger.error(f"Application failed to start: {str(e)}")
31
+ raise
32
+
33
+ def _init_enterprise_components(self) -> None:
34
+ """初始化企业级组件"""
35
+ self.logger.info("Initializing enterprise components...")
36
+
37
+ # 从配置文件和环境变量加载配置
38
+ config = self.application_context.get_config()
39
+ fail_fast = self._should_fail_fast(config)
40
+
41
+ # 生产环境安全检查
42
+ if fail_fast:
43
+ self._production_security_check(config)
44
+
45
+ # 解析配置中的密钥引用 (${secret:xxx})
46
+ try:
47
+ from spring.security.secret_manager import resolve_secret_config
48
+ resolved = resolve_secret_config(config)
49
+ config.update(resolved)
50
+ self.application_context._config = resolved
51
+ except Exception as e:
52
+ self.logger.debug(f"Secret resolution skipped: {e}")
53
+
54
+ # 初始化日志
55
+ init_logging(config.get('logging', {}))
56
+
57
+ # 初始化Redis(延迟导入)
58
+ if config.get('redis', {}).get('enabled', True):
59
+ try:
60
+ from spring.utils.redis_client import init_redis
61
+ init_redis(config.get('redis', {}))
62
+ self.logger.info("Redis initialized")
63
+ except ImportError:
64
+ if fail_fast:
65
+ raise RuntimeError("Redis已启用但redis依赖未安装")
66
+ self.logger.warning("Redis not available (redis package not installed)")
67
+ except Exception as e:
68
+ if fail_fast:
69
+ raise RuntimeError("Redis初始化失败") from e
70
+ self.logger.warning(f"Failed to initialize Redis: {e}")
71
+
72
+ # 初始化JWT(延迟导入)
73
+ try:
74
+ from spring.security.jwt_utils import init_jwt
75
+ init_jwt(config.get('jwt', {}))
76
+ self.logger.info("JWT initialized")
77
+ except ImportError:
78
+ raise RuntimeError("JWT核心依赖pyjwt未安装")
79
+
80
+ # 初始化数据库(延迟导入)
81
+ database_config = config.get('database', {})
82
+ orm_mode = str(database_config.get('orm', 'mybatis')).lower()
83
+ if database_config.get('enabled', False) and orm_mode in {'sqlalchemy', 'both'}:
84
+ try:
85
+ from spring.orm.database import init_database
86
+ init_database(config.get('database', {}))
87
+ self.logger.info("Database initialized")
88
+ except ImportError:
89
+ if fail_fast:
90
+ raise RuntimeError("SQLAlchemy已启用但依赖未安装")
91
+ self.logger.warning("Database not available (sqlalchemy not installed)")
92
+ except Exception as e:
93
+ if fail_fast:
94
+ raise RuntimeError("SQLAlchemy数据库初始化失败") from e
95
+ self.logger.warning(f"Failed to initialize Database: {e}")
96
+
97
+ # 初始化Nacos服务发现(延迟导入)
98
+ discovery_enabled = config.get('discovery', {}).get('enabled', False) or any(
99
+ isinstance(item, EnableDiscoveryClient)
100
+ for item in getattr(self.main_class, '__spring_annotations__', [])
101
+ )
102
+ if discovery_enabled:
103
+ try:
104
+ from spring.cloud.discovery import init_discovery
105
+ init_discovery(config.get('discovery', {}))
106
+ self.logger.info("Service discovery initialized")
107
+ except ImportError:
108
+ if fail_fast:
109
+ raise RuntimeError("服务发现已启用但Nacos依赖未安装")
110
+ self.logger.warning("Service discovery not available (nacos-sdk-python not installed)")
111
+ except Exception as e:
112
+ if fail_fast:
113
+ raise RuntimeError("服务发现初始化失败") from e
114
+ self.logger.warning(f"Failed to initialize Service Discovery: {e}")
115
+
116
+ # 初始化Seata分布式事务(延迟导入)
117
+ if config.get('seata', {}).get('enabled', False):
118
+ try:
119
+ from spring.cloud.seata import init_seata
120
+ init_seata(config.get('seata', {}))
121
+ self.logger.info("Seata distributed transaction initialized")
122
+ except ImportError:
123
+ if fail_fast:
124
+ raise RuntimeError("Seata已启用但依赖未安装")
125
+ self.logger.warning("Seata not available")
126
+ except Exception as e:
127
+ if fail_fast:
128
+ raise RuntimeError("Seata初始化失败") from e
129
+ self.logger.warning(f"Failed to initialize Seata: {e}")
130
+
131
+ # 初始化RabbitMQ(延迟导入)
132
+ if config.get('rabbitmq', {}).get('enabled', False):
133
+ try:
134
+ from spring.messaging.rabbitmq import init_rabbitmq
135
+ init_rabbitmq(config.get('rabbitmq', {}))
136
+ self.logger.info("RabbitMQ initialized")
137
+ except ImportError:
138
+ if fail_fast:
139
+ raise RuntimeError("RabbitMQ已启用但pika依赖未安装")
140
+ self.logger.warning("RabbitMQ not available (pika not installed)")
141
+ except Exception as e:
142
+ if fail_fast:
143
+ raise RuntimeError("RabbitMQ初始化失败") from e
144
+ self.logger.warning(f"Failed to initialize RabbitMQ: {e}")
145
+
146
+ # 初始化Prometheus监控(延迟导入)
147
+ if config.get('prometheus', {}).get('enabled', False):
148
+ try:
149
+ from spring.monitoring.prometheus import init_prometheus
150
+ init_prometheus(config.get('prometheus', {}))
151
+ self.logger.info("Prometheus monitoring initialized")
152
+ except ImportError:
153
+ if fail_fast:
154
+ raise RuntimeError("Prometheus已启用但依赖未安装")
155
+ self.logger.warning("Prometheus not available (prometheus-client not installed)")
156
+ except Exception as e:
157
+ if fail_fast:
158
+ raise RuntimeError("Prometheus初始化失败") from e
159
+ self.logger.warning(f"Failed to initialize Prometheus: {e}")
160
+
161
+ self.logger.info("Enterprise components initialization completed")
162
+
163
+ def _prepare_context(self) -> None:
164
+ self.logger.info("Preparing application context...")
165
+ self.application_context = ApplicationContext(self.main_class)
166
+ self._init_enterprise_components()
167
+ config = self.application_context.get_config()
168
+ fail_fast = self._should_fail_fast(config)
169
+
170
+ # 在refresh之前先初始化MyBatis,确保Mapper在组件扫描时可用
171
+ try:
172
+ init_mybatis(self.application_context)
173
+ self.logger.info("MyBatis integration initialized")
174
+ except Exception as e:
175
+ if fail_fast:
176
+ raise RuntimeError("MyBatis初始化失败") from e
177
+ self.logger.warning(f"Failed to initialize MyBatis integration: {e}")
178
+
179
+ self.application_context.refresh()
180
+
181
+ self.logger.info(f"Registered {self.application_context.bean_factory.get_bean_count()} beans")
182
+
183
+ def _on_app_startup(self) -> None:
184
+ """在 ASGI worker 已启动后创建后台线程并注册服务。"""
185
+ if self._background_started:
186
+ return
187
+ config = self.application_context.get_config()
188
+ fail_fast = self._should_fail_fast(config)
189
+ if config.get('rabbitmq', {}).get('enabled', False):
190
+ try:
191
+ from spring.messaging.rabbitmq import rabbitmq_client
192
+ rabbitmq_client.start_consuming_background()
193
+ except Exception as exc:
194
+ if fail_fast:
195
+ raise RuntimeError("RabbitMQ消费者启动失败") from exc
196
+ self.logger.warning(f"Failed to start RabbitMQ consumers: {exc}")
197
+ port = config.get('server', {}).get('port', 8080)
198
+ self._register_discovery_service(port)
199
+ self._background_started = True
200
+
201
+ def _configure_web_lifecycle(self) -> None:
202
+ app = self.web_context.fastapi_app
203
+ app.router.add_event_handler('startup', self._on_app_startup)
204
+ app.router.add_event_handler('shutdown', self._deregister_discovery_service)
205
+ app.router.add_event_handler('shutdown', self._on_app_shutdown)
206
+
207
+ @staticmethod
208
+ def _should_fail_fast(config: dict) -> bool:
209
+ startup_config = config.get('startup', {})
210
+ if 'fail_fast' in startup_config:
211
+ return bool(startup_config['fail_fast'])
212
+ profile = str(config.get('spring', {}).get('profiles', {}).get('active', 'default')).lower()
213
+ return profile in {'prod', 'production'}
214
+
215
+ def _production_security_check(self, config: dict) -> None:
216
+ """生产环境安全检查"""
217
+ warnings = []
218
+ # 检查JWT密钥是否为默认值
219
+ jwt_config = config.get('jwt', {})
220
+ jwt_secret = jwt_config.get('secret_key', '')
221
+ if jwt_secret in ('', 'your-secret-key', 'secret', 'changeme', 'springpy-secret'):
222
+ warnings.append("JWT secret_key is default/empty, MUST set strong secret in production")
223
+
224
+ # 检查数据库是否无密码
225
+ db_config = config.get('database', {})
226
+ if db_config.get('enabled', False) and not db_config.get('password'):
227
+ warnings.append("Database password is empty, MUST set password in production")
228
+
229
+ # 检查CORS是否全开
230
+ cors_config = config.get('cors', {})
231
+ allow_origins = cors_config.get('allow_origins', [])
232
+ if '*' in (allow_origins if isinstance(allow_origins, list) else [allow_origins]):
233
+ warnings.append("CORS allows all origins (*), restrict to specific domains in production")
234
+
235
+ # 检查是否启用了debug模式
236
+ if config.get('debug', False) or config.get('server', {}).get('debug', False):
237
+ warnings.append("Debug mode is enabled, MUST disable in production")
238
+
239
+ # 检查Docker IP自动检测
240
+ import os
241
+ if not os.getenv('SPRING_DISABLE_DOCKER_IP_DETECT'):
242
+ warnings.append("SPRING_DISABLE_DOCKER_IP_DETECT not set, should be 1 in production")
243
+
244
+ if warnings:
245
+ for w in warnings:
246
+ self.logger.warning(f"[PROD-SECURITY] {w}")
247
+
248
+ def _start_web_server(self, **kwargs) -> None:
249
+ self.logger.info("Initializing web context...")
250
+ self.web_context = WebApplicationContext(self.application_context)
251
+ self.web_context.init()
252
+ self._configure_web_lifecycle()
253
+
254
+ # 注册优雅退出信号处理
255
+ try:
256
+ from spring.core.graceful_shutdown import shutdown_handler
257
+ shutdown_handler.register_signals()
258
+ # 注册资源关闭钩子
259
+ shutdown_handler.register_hook("discovery_deregister", self._deregister_discovery_service, order=10)
260
+ except Exception:
261
+ pass
262
+
263
+ # 从配置获取端口和主机
264
+ config = self.application_context.get_config()
265
+ server_config = config.get('server', {})
266
+
267
+ port = kwargs.get('port', server_config.get('port', 8080))
268
+ host = kwargs.get('host', server_config.get('host', '0.0.0.0'))
269
+
270
+ banner = BannerPrinter()
271
+ banner.print_startup_info(port)
272
+
273
+ self.web_context.run(host=host, port=port)
274
+
275
+ def _on_app_shutdown(self):
276
+ """ASGI应用关闭事件回调"""
277
+ try:
278
+ from spring.core.graceful_shutdown import shutdown_handler
279
+ if not shutdown_handler._signal_received:
280
+ # 如果是ASGI服务器直接关闭(非信号触发),执行关闭钩子
281
+ shutdown_handler.initiate_shutdown()
282
+ except Exception:
283
+ pass
284
+
285
+ def _register_discovery_service(self, port: int) -> None:
286
+ """Register the running application after its HTTP port is known."""
287
+ annotations = getattr(self.main_class, '__spring_annotations__', [])
288
+ enabled = any(isinstance(item, EnableDiscoveryClient) for item in annotations)
289
+ config = self.application_context.get_config()
290
+ discovery_config = config.get('discovery', {})
291
+ if not enabled and not discovery_config.get('enabled', False):
292
+ return
293
+ service_name = (
294
+ config.get('spring', {}).get('application', {}).get('name')
295
+ or config.get('application', {}).get('name')
296
+ )
297
+ if not service_name:
298
+ self.logger.warning("Discovery enabled but spring.application.name is missing")
299
+ return
300
+ try:
301
+ from spring.cloud import discovery
302
+ ip = discovery_config.get('ip') or discovery_config.get('host')
303
+ if not ip or ip in {'0.0.0.0', '::'}:
304
+ ip = '127.0.0.1'
305
+ try:
306
+ ip = socket.gethostbyname(socket.gethostname())
307
+ except OSError:
308
+ pass
309
+ if discovery.nacos_client.register_service(
310
+ service_name,
311
+ ip,
312
+ int(port),
313
+ metadata=discovery_config.get('metadata', {}),
314
+ ):
315
+ self._discovery_registered = True
316
+ except Exception as exc:
317
+ self.logger.warning(f"Service discovery registration failed: {exc}")
318
+
319
+ def _deregister_discovery_service(self) -> None:
320
+ if not self._discovery_registered or self.application_context is None:
321
+ return
322
+ try:
323
+ from spring.cloud import discovery
324
+ service_name = self.application_context.get_value('spring.application.name')
325
+ if service_name:
326
+ discovery.nacos_client.deregister_service(
327
+ service_name,
328
+ discovery.nacos_client._ip,
329
+ discovery.nacos_client._port,
330
+ )
331
+ except Exception as exc:
332
+ self.logger.warning(f"Service discovery deregistration failed: {exc}")
333
+ finally:
334
+ self._discovery_registered = False
335
+
336
+
337
+ def run(main_class: Type, **kwargs) -> None:
338
+ app = SpringApplication(main_class)
339
+ app.run(**kwargs)
340
+
341
+
342
+ def create_app(main_class: Type):
343
+ """构建ASGI应用,供Uvicorn/Gunicorn等生产进程管理器加载。"""
344
+ application = SpringApplication(main_class)
345
+ application._prepare_context()
346
+ application.web_context = WebApplicationContext(application.application_context)
347
+ application.web_context.init()
348
+ application._configure_web_lifecycle()
349
+ asgi_app = application.web_context.get_app()
350
+ asgi_app.state.spring_application = application
351
+ return asgi_app
352
+
353
+
354
+ def run_cli():
355
+ """CLI entry point for springboot-python"""
356
+ import argparse
357
+ import importlib
358
+
359
+ parser = argparse.ArgumentParser(description="SpringBoot-Python CLI")
360
+ parser.add_argument('module', help='Application module path (e.g., myapp.Application)')
361
+ parser.add_argument('--port', type=int, default=8080, help='Server port')
362
+ parser.add_argument('--host', default='0.0.0.0', help='Server host')
363
+
364
+ args = parser.parse_args()
365
+
366
+ # Split module path
367
+ if '.' in args.module:
368
+ module_name, class_name = args.module.rsplit('.', 1)
369
+ else:
370
+ module_name = args.module
371
+ class_name = 'Application'
372
+
373
+ # Import module and get class
374
+ module = importlib.import_module(module_name)
375
+ main_class = getattr(module, class_name)
376
+
377
+ # Run application
378
+ run(main_class, port=args.port, host=args.host)
@@ -0,0 +1 @@
1
+ """Messaging integrations."""
@@ -0,0 +1,302 @@
1
+ """
2
+ 消息队列模块
3
+ 集成RabbitMQ实现异步消息处理
4
+ """
5
+ import pika
6
+ import asyncio
7
+ import inspect
8
+ import json
9
+ import logging
10
+ import threading
11
+ from typing import Callable, Dict, Any, Optional
12
+
13
+ logger = logging.getLogger("Spring.Messaging.RabbitMQ")
14
+
15
+
16
+ class RabbitMQClient:
17
+ """RabbitMQ客户端"""
18
+
19
+ _instance = None
20
+ _lock = threading.Lock()
21
+
22
+ def __new__(cls, *args, **kwargs):
23
+ if cls._instance is None:
24
+ with cls._lock:
25
+ if cls._instance is None:
26
+ cls._instance = super().__new__(cls)
27
+ return cls._instance
28
+
29
+ def __init__(self, host: str = "localhost", port: int = 5672,
30
+ username: str = "guest", password: str = "guest",
31
+ virtual_host: str = "/"):
32
+ if hasattr(self, '_initialized'):
33
+ return
34
+ self.host = host
35
+ self.port = port
36
+ self.username = username
37
+ self.password = password
38
+ self.virtual_host = virtual_host
39
+ self._connection: Optional[pika.BlockingConnection] = None
40
+ self._channel: Optional[pika.channel.Channel] = None
41
+ self._consumers: Dict[str, Callable] = {}
42
+ self._consumer_thread: Optional[threading.Thread] = None
43
+ self._initialized = True
44
+
45
+ def connect(self) -> None:
46
+ """连接RabbitMQ"""
47
+ try:
48
+ credentials = pika.PlainCredentials(self.username, self.password)
49
+ parameters = pika.ConnectionParameters(
50
+ host=self.host,
51
+ port=self.port,
52
+ credentials=credentials,
53
+ virtual_host=self.virtual_host,
54
+ heartbeat=600,
55
+ blocked_connection_timeout=300,
56
+ )
57
+
58
+ self._connection = pika.BlockingConnection(parameters)
59
+ self._channel = self._connection.channel()
60
+
61
+ logger.info(f"Connected to RabbitMQ: {self.host}:{self.port}")
62
+ except Exception as e:
63
+ logger.error(f"Failed to connect to RabbitMQ: {e}")
64
+ raise
65
+
66
+ def get_channel(self) -> pika.channel.Channel:
67
+ """获取通道"""
68
+ if self._channel is None:
69
+ self.connect()
70
+ return self._channel
71
+
72
+ def declare_queue(self, queue_name: str, durable: bool = True,
73
+ exclusive: bool = False, auto_delete: bool = False) -> None:
74
+ """
75
+ 声明队列
76
+
77
+ Args:
78
+ queue_name: 队列名称
79
+ durable: 是否持久化
80
+ exclusive: 是否排他
81
+ auto_delete: 是否自动删除
82
+ """
83
+ channel = self.get_channel()
84
+ channel.queue_declare(
85
+ queue=queue_name,
86
+ durable=durable,
87
+ exclusive=exclusive,
88
+ auto_delete=auto_delete,
89
+ )
90
+ logger.info(f"Declared queue: {queue_name}")
91
+
92
+ def declare_exchange(self, exchange_name: str, exchange_type: str = "direct",
93
+ durable: bool = True) -> None:
94
+ """
95
+ 声明交换机
96
+
97
+ Args:
98
+ exchange_name: 交换机名称
99
+ exchange_type: 交换机类型
100
+ durable: 是否持久化
101
+ """
102
+ channel = self.get_channel()
103
+ channel.exchange_declare(
104
+ exchange=exchange_name,
105
+ exchange_type=exchange_type,
106
+ durable=durable,
107
+ )
108
+ logger.info(f"Declared exchange: {exchange_name}")
109
+
110
+ def bind_queue(self, queue_name: str, exchange_name: str, routing_key: str = "") -> None:
111
+ """
112
+ 绑定队列到交换机
113
+
114
+ Args:
115
+ queue_name: 队列名称
116
+ exchange_name: 交换机名称
117
+ routing_key: 路由键
118
+ """
119
+ channel = self.get_channel()
120
+ channel.queue_bind(
121
+ queue=queue_name,
122
+ exchange=exchange_name,
123
+ routing_key=routing_key,
124
+ )
125
+ logger.info(f"Bound queue {queue_name} to exchange {exchange_name}")
126
+
127
+ def publish(self, exchange_name: str, routing_key: str, body: Any,
128
+ content_type: str = "application/json", persistent: bool = True) -> None:
129
+ """
130
+ 发布消息
131
+
132
+ Args:
133
+ exchange_name: 交换机名称
134
+ routing_key: 路由键
135
+ body: 消息体
136
+ content_type: 内容类型
137
+ persistent: 是否持久化
138
+ """
139
+ channel = self.get_channel()
140
+
141
+ # 序列化消息体
142
+ if isinstance(body, dict):
143
+ body_str = json.dumps(body)
144
+ else:
145
+ body_str = str(body)
146
+
147
+ # 发布消息
148
+ channel.basic_publish(
149
+ exchange=exchange_name,
150
+ routing_key=routing_key,
151
+ body=body_str,
152
+ properties=pika.BasicProperties(
153
+ content_type=content_type,
154
+ delivery_mode=2 if persistent else 1,
155
+ ),
156
+ )
157
+ logger.debug(f"Published message to {exchange_name}:{routing_key}")
158
+
159
+ def publish_to_queue(self, queue_name: str, body: Any,
160
+ content_type: str = "application/json",
161
+ persistent: bool = True) -> None:
162
+ """
163
+ 直接发布消息到队列
164
+
165
+ Args:
166
+ queue_name: 队列名称
167
+ body: 消息体
168
+ content_type: 内容类型
169
+ persistent: 是否持久化
170
+ """
171
+ self.publish(
172
+ exchange_name="",
173
+ routing_key=queue_name,
174
+ body=body,
175
+ content_type=content_type,
176
+ persistent=persistent,
177
+ )
178
+
179
+ def consume(self, queue_name: str, callback: Callable,
180
+ auto_ack: bool = False, prefetch_count: int = 1) -> None:
181
+ """
182
+ 消费消息
183
+
184
+ Args:
185
+ queue_name: 队列名称
186
+ callback: 回调函数
187
+ auto_ack: 是否自动确认
188
+ prefetch_count: 预取数量
189
+ """
190
+ channel = self.get_channel()
191
+
192
+ # 设置预取数量
193
+ channel.basic_qos(prefetch_count=prefetch_count)
194
+
195
+ # 注册回调
196
+ self._consumers[queue_name] = callback
197
+
198
+ # 开始消费
199
+ channel.basic_consume(
200
+ queue=queue_name,
201
+ on_message_callback=self._create_message_handler(callback, auto_ack),
202
+ auto_ack=auto_ack,
203
+ )
204
+
205
+ logger.info(f"Started consuming queue: {queue_name}")
206
+
207
+ def _create_message_handler(self, callback: Callable, auto_ack: bool = False) -> Callable:
208
+ """创建消息处理器"""
209
+ def handler(ch, method, properties, body):
210
+ try:
211
+ # 解析消息体
212
+ try:
213
+ message = json.loads(body) if body else None
214
+ except json.JSONDecodeError:
215
+ message = body.decode('utf-8') if body else None
216
+
217
+ # 调用回调
218
+ result = callback(message)
219
+ if inspect.isawaitable(result):
220
+ asyncio.run(result)
221
+
222
+ # 手动确认
223
+ if not auto_ack:
224
+ ch.basic_ack(delivery_tag=method.delivery_tag)
225
+
226
+ logger.debug(f"Processed message: {message}")
227
+ except Exception as e:
228
+ logger.error(f"Failed to process message: {e}")
229
+
230
+ # 处理失败时重新入队
231
+ if not auto_ack:
232
+ ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
233
+
234
+ return handler
235
+
236
+ def start_consuming(self) -> None:
237
+ """开始消费(阻塞式)"""
238
+ if not self._consumers:
239
+ return
240
+ logger.info("Starting message consumption...")
241
+ self.get_channel().start_consuming()
242
+
243
+ def start_consuming_background(self) -> Optional[threading.Thread]:
244
+ """在守护线程中启动已注册的消费者。"""
245
+ if not self._consumers:
246
+ return None
247
+ if self._consumer_thread and self._consumer_thread.is_alive():
248
+ return self._consumer_thread
249
+
250
+ self._consumer_thread = threading.Thread(
251
+ target=self.start_consuming,
252
+ name="SpringRabbitConsumer",
253
+ daemon=True,
254
+ )
255
+ self._consumer_thread.start()
256
+ return self._consumer_thread
257
+
258
+ def stop_consuming(self) -> None:
259
+ """停止消费"""
260
+ if self._channel and self._consumer_thread and self._consumer_thread.is_alive():
261
+ if self._connection and not self._connection.is_closed:
262
+ self._connection.add_callback_threadsafe(self._channel.stop_consuming)
263
+ if threading.current_thread() is not self._consumer_thread:
264
+ self._consumer_thread.join(timeout=5)
265
+ elif self._channel and getattr(self._channel, 'is_open', False):
266
+ self._channel.stop_consuming()
267
+ logger.info("Stopped message consumption")
268
+
269
+ def close(self) -> None:
270
+ """关闭连接"""
271
+ self.stop_consuming()
272
+ if self._connection and not self._connection.is_closed:
273
+ self._connection.close()
274
+ logger.info("Closed RabbitMQ connection")
275
+ self._connection = None
276
+ self._channel = None
277
+ self._consumer_thread = None
278
+
279
+
280
+ # 创建全局RabbitMQ客户端实例
281
+ rabbitmq_client = RabbitMQClient()
282
+
283
+
284
+ def init_rabbitmq(config: dict) -> None:
285
+ """
286
+ 初始化RabbitMQ配置
287
+
288
+ Args:
289
+ config: 配置字典,包含host, port, username, password等
290
+ """
291
+ # Preserve references imported by RabbitTemplate and listener registration.
292
+ rabbitmq_client.close()
293
+ rabbitmq_client.host = config.get('host', 'localhost')
294
+ rabbitmq_client.port = int(config.get('port', 5672))
295
+ rabbitmq_client.username = config.get('username', 'guest')
296
+ rabbitmq_client.password = config.get('password', 'guest')
297
+ rabbitmq_client.virtual_host = config.get('virtual_host', '/')
298
+ rabbitmq_client._connection = None
299
+ rabbitmq_client._channel = None
300
+ rabbitmq_client._consumers.clear()
301
+ rabbitmq_client._consumer_thread = None
302
+ rabbitmq_client.connect()
@@ -0,0 +1 @@
1
+ """Metrics and monitoring integrations."""