springbootAI 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spring/__init__.py +66 -0
- spring/ai/__init__.py +78 -0
- spring/ai/advisors.py +139 -0
- spring/ai/annotations.py +74 -0
- spring/ai/autoconfig.py +481 -0
- spring/ai/core.py +391 -0
- spring/ai/etl.py +188 -0
- spring/ai/memory.py +109 -0
- spring/ai/observability.py +129 -0
- spring/ai/providers.py +789 -0
- spring/ai/resilience.py +258 -0
- spring/ai/tools.py +106 -0
- spring/ai/vectorstore.py +303 -0
- spring/annotations/__init__.py +188 -0
- spring/annotations/cache.py +126 -0
- spring/annotations/cloud.py +207 -0
- spring/annotations/conditional.py +272 -0
- spring/annotations/core.py +864 -0
- spring/annotations/messaging.py +107 -0
- spring/aop/__init__.py +4 -0
- spring/aop/cloud_aop.py +404 -0
- spring/aop/comprehensive_aop.py +1015 -0
- spring/aop/method_interceptor.py +19 -0
- spring/aop/proxy_factory.py +55 -0
- spring/cloud/__init__.py +76 -0
- spring/cloud/discovery.py +364 -0
- spring/cloud/feign.py +469 -0
- spring/cloud/gateway.py +452 -0
- spring/cloud/load_balancer.py +149 -0
- spring/cloud/seata.py +557 -0
- spring/cloud/sentinel.py +525 -0
- spring/cloud/tracer.py +337 -0
- spring/config/__init__.py +21 -0
- spring/config/binding.py +206 -0
- spring/config/config_loader.py +405 -0
- spring/context/__init__.py +13 -0
- spring/context/application_context.py +589 -0
- spring/context/bean_definition.py +70 -0
- spring/context/bean_factory.py +1052 -0
- spring/context/registry.py +58 -0
- spring/context/scanner.py +106 -0
- spring/core/__init__.py +3 -0
- spring/core/graceful_shutdown.py +196 -0
- spring/core/typing_utils.py +50 -0
- spring/csv/__init__.py +52 -0
- spring/csv/annotations.py +402 -0
- spring/csv/converters.py +69 -0
- spring/csv/easy_csv.py +95 -0
- spring/csv/exceptions.py +27 -0
- spring/csv/reader.py +195 -0
- spring/csv/writer.py +155 -0
- spring/data/__init__.py +54 -0
- spring/data/page.py +181 -0
- spring/data/repository.py +274 -0
- spring/data/specification.py +228 -0
- spring/datasource/__init__.py +66 -0
- spring/datasource/annotations.py +133 -0
- spring/datasource/context.py +69 -0
- spring/datasource/dynamic.py +148 -0
- spring/event/__init__.py +7 -0
- spring/event/publisher.py +69 -0
- spring/excel/__init__.py +51 -0
- spring/excel/annotations.py +405 -0
- spring/excel/converters.py +231 -0
- spring/excel/easy_excel.py +94 -0
- spring/excel/exceptions.py +31 -0
- spring/excel/reader.py +254 -0
- spring/excel/style.py +95 -0
- spring/excel/writer.py +197 -0
- spring/i18n/__init__.py +97 -0
- spring/i18n/accessor.py +94 -0
- spring/i18n/auto_config.py +177 -0
- spring/i18n/holder.py +106 -0
- spring/i18n/locale.py +152 -0
- spring/i18n/locale_resolver.py +367 -0
- spring/i18n/message_source.py +250 -0
- spring/i18n/middleware.py +79 -0
- spring/i18n/properties.py +168 -0
- spring/i18n/sources.py +255 -0
- spring/logging/__init__.py +1 -0
- spring/logging/loguru_logger.py +228 -0
- spring/main.py +378 -0
- spring/messaging/__init__.py +1 -0
- spring/messaging/rabbitmq.py +302 -0
- spring/monitoring/__init__.py +1 -0
- spring/monitoring/prometheus.py +199 -0
- spring/orm/__init__.py +258 -0
- spring/orm/database.py +222 -0
- spring/orm/ddl_auto.py +1217 -0
- spring/orm/migration.py +419 -0
- spring/orm/mybatis_integration.py +400 -0
- spring/orm/pymybatis/__init__.py +86 -0
- spring/orm/pymybatis/annotations/__init__.py +30 -0
- spring/orm/pymybatis/annotations/annotations.py +332 -0
- spring/orm/pymybatis/cache/__init__.py +47 -0
- spring/orm/pymybatis/cache/cache.py +371 -0
- spring/orm/pymybatis/cache/redis_cache.py +434 -0
- spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
- spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
- spring/orm/pymybatis/configuration.py +525 -0
- spring/orm/pymybatis/core/__init__.py +10 -0
- spring/orm/pymybatis/core/sql_session.py +1382 -0
- spring/orm/pymybatis/core/sql_session_factory.py +76 -0
- spring/orm/pymybatis/dialect/__init__.py +9 -0
- spring/orm/pymybatis/dialect/dialect.py +445 -0
- spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
- spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
- spring/orm/pymybatis/interceptor/__init__.py +31 -0
- spring/orm/pymybatis/interceptor/interceptor.py +427 -0
- spring/orm/pymybatis/mapper/__init__.py +9 -0
- spring/orm/pymybatis/mapper/mapper.py +540 -0
- spring/orm/pymybatis/metrics/__init__.py +41 -0
- spring/orm/pymybatis/metrics/metrics.py +595 -0
- spring/orm/pymybatis/pool/__init__.py +9 -0
- spring/orm/pymybatis/pool/connection_pool.py +711 -0
- spring/orm/pymybatis/security/__init__.py +19 -0
- spring/orm/pymybatis/security/access_control.py +415 -0
- spring/orm/pymybatis/security/password_encoder.py +293 -0
- spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
- spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
- spring/orm/pymybatis/transaction/__init__.py +9 -0
- spring/orm/pymybatis/transaction/transaction.py +288 -0
- spring/orm/pymybatis/type_handler/__init__.py +37 -0
- spring/orm/pymybatis/type_handler/type_handler.py +473 -0
- spring/orm/pymybatis/version.py +9 -0
- spring/orm/pymybatis/xml_parser/__init__.py +9 -0
- spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
- spring/retry/__init__.py +12 -0
- spring/retry/retry_annotations.py +71 -0
- spring/retry/retry_decorator.py +155 -0
- spring/scheduling/__init__.py +3 -0
- spring/scheduling/scheduler.py +389 -0
- spring/security/__init__.py +39 -0
- spring/security/jwt_utils.py +281 -0
- spring/security/replay_protection.py +206 -0
- spring/security/secret_manager.py +226 -0
- spring/security/security_aop.py +248 -0
- spring/security/security_context.py +172 -0
- spring/test/__init__.py +45 -0
- spring/test/slicing.py +341 -0
- spring/tracing/__init__.py +11 -0
- spring/tracing/skywalking.py +229 -0
- spring/tx/__init__.py +52 -0
- spring/tx/events.py +172 -0
- spring/tx/synchronization.py +143 -0
- spring/utils/__init__.py +5 -0
- spring/utils/banner.py +32 -0
- spring/utils/logger.py +73 -0
- spring/utils/redis_client.py +526 -0
- spring/validation/__init__.py +55 -0
- spring/validation/aop.py +141 -0
- spring/validation/constraints.py +357 -0
- spring/validation/exceptions.py +55 -0
- spring/validation/validator.py +139 -0
- spring/web/__init__.py +12 -0
- spring/web/actuator.py +319 -0
- spring/web/exception_handler.py +61 -0
- spring/web/health.py +399 -0
- spring/web/interceptor.py +91 -0
- spring/web/result.py +44 -0
- spring/web/swagger.py +601 -0
- spring/web/web_context.py +755 -0
- spring/websocket/__init__.py +86 -0
- spring/websocket/annotations.py +169 -0
- spring/websocket/broker.py +238 -0
- spring/websocket/exceptions.py +26 -0
- spring/websocket/handler.py +243 -0
- spring/websocket/router.py +526 -0
- spring/websocket/session.py +216 -0
- springbootai-1.8.0.dist-info/METADATA +2796 -0
- springbootai-1.8.0.dist-info/RECORD +175 -0
- springbootai-1.8.0.dist-info/WHEEL +5 -0
- springbootai-1.8.0.dist-info/entry_points.txt +2 -0
- springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
- springbootai-1.8.0.dist-info/top_level.txt +1 -0
spring/test/slicing.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Spring Boot 风格测试切片(对齐 ``@SpringBootTest`` / ``@WebMvcTest`` / ``@DataJpaTest``)。
|
|
2
|
+
|
|
3
|
+
提供三种测试上下文切片,复用既有 ``ApplicationContext`` / ``WebApplicationContext`` /
|
|
4
|
+
``DdlAutoManager`` / ``PagingAndSortingRepository`` 基础设施:
|
|
5
|
+
|
|
6
|
+
- **``SpringBootTest``**:全量应用上下文(扫描+装配所有 Bean),对齐 ``@SpringBootTest``。
|
|
7
|
+
- **``WebMvcTest``**:仅 Web 切片——只注册指定 Controller(依赖用 ``MagicMock`` 注入),
|
|
8
|
+
返回 FastAPI ``TestClient``,对齐 ``@WebMvcTest``。
|
|
9
|
+
- **``DataJpaTest``**:仅数据切片——内存 SQLite + ``DdlAutoManager`` 建表 +
|
|
10
|
+
``PagingAndSortingRepository`` 工厂,对齐 ``@DataJpaTest``。
|
|
11
|
+
|
|
12
|
+
设计原则:**复用既有范式**,不重复造轮子;切片上下文提供 ``close()`` 清理,便于 pytest 夹具使用。
|
|
13
|
+
|
|
14
|
+
与 Java 的差异:
|
|
15
|
+
- Spring Boot 切片用专用 ``ApplicationContextInitializer`` 裁剪自动配置;本实现通过手动注册
|
|
16
|
+
指定 Bean + Mock 依赖实现等价裁剪,更轻量。
|
|
17
|
+
- ``@WebMvcTest`` 在 Spring 中自动 Mock ``@Service``/``@Repository``;本实现 Mock 构造函数依赖。
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import inspect
|
|
22
|
+
import os
|
|
23
|
+
import sqlite3
|
|
24
|
+
import tempfile
|
|
25
|
+
from typing import Any, Dict, List, Optional, Type
|
|
26
|
+
from unittest.mock import MagicMock
|
|
27
|
+
|
|
28
|
+
import yaml
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ==================== 通用:内存连接池(DataJpaTest 复用)====================
|
|
32
|
+
|
|
33
|
+
class _PooledConn:
|
|
34
|
+
"""连接池包装:``close()`` 为 no-op(池化语义),其余委托底层连接。"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, conn):
|
|
37
|
+
self._conn = conn
|
|
38
|
+
|
|
39
|
+
def cursor(self, *a, **k):
|
|
40
|
+
return self._conn.cursor(*a, **k)
|
|
41
|
+
|
|
42
|
+
def commit(self):
|
|
43
|
+
return self._conn.commit()
|
|
44
|
+
|
|
45
|
+
def rollback(self):
|
|
46
|
+
return self._conn.rollback()
|
|
47
|
+
|
|
48
|
+
def close(self):
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def __getattr__(self, item):
|
|
52
|
+
return getattr(self._conn, item)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _DbutilsPooled:
|
|
56
|
+
"""DBUtils 风格池化连接:``.connection`` 暴露底层连接(``DdlAutoManager`` 期望)。"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, conn):
|
|
59
|
+
self.connection = conn
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestPool:
|
|
63
|
+
"""三接口内存连接池:
|
|
64
|
+
- ``get_connection``/``return_connection`` + ``.connection``:``DdlAutoManager``
|
|
65
|
+
- ``connection()``:``PagingAndSortingRepository``
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, conn):
|
|
69
|
+
self._conn = conn
|
|
70
|
+
|
|
71
|
+
def get_connection(self):
|
|
72
|
+
return _DbutilsPooled(self._conn)
|
|
73
|
+
|
|
74
|
+
def return_connection(self, pooled):
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def connection(self):
|
|
78
|
+
return _PooledConn(self._conn)
|
|
79
|
+
|
|
80
|
+
def get_pool_stats(self):
|
|
81
|
+
return {"dialect": "sqlite"}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ==================== 工具 ====================
|
|
85
|
+
|
|
86
|
+
def _write_temp_config(config: Dict[str, Any]) -> str:
|
|
87
|
+
"""把配置字典写到临时 yml 文件,返回路径。"""
|
|
88
|
+
fd, path = tempfile.mkstemp(suffix=".yml", prefix="springboot_test_")
|
|
89
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
90
|
+
yaml.safe_dump(config or {}, f, allow_unicode=True)
|
|
91
|
+
return path
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _instantiate_with_mocks(cls: Type) -> Any:
|
|
95
|
+
"""实例化类,所有非 self 构造参数用 ``MagicMock`` 注入(``@WebMvcTest`` Mock 依赖)。
|
|
96
|
+
|
|
97
|
+
无论参数是否有默认值都注入 Mock,确保 ``@Autowired`` 依赖被替换为可控桩对象,
|
|
98
|
+
对齐 Spring ``@WebMvcTest`` 自动 Mock ``@Service``/``@Repository`` 的语义。
|
|
99
|
+
"""
|
|
100
|
+
sig = inspect.signature(cls.__init__)
|
|
101
|
+
kwargs = {}
|
|
102
|
+
for name, param in sig.parameters.items():
|
|
103
|
+
if name == "self":
|
|
104
|
+
continue
|
|
105
|
+
# *args / **kwargs 不注入
|
|
106
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
107
|
+
continue
|
|
108
|
+
kwargs[name] = MagicMock()
|
|
109
|
+
return cls(**kwargs)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ==================== SpringBootTest(全量上下文)====================
|
|
113
|
+
|
|
114
|
+
class SpringBootTest:
|
|
115
|
+
"""全量应用上下文测试切片(对齐 ``@SpringBootTest``)。
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
main_class: ``@SpringBootApplication`` 标注的入口类。
|
|
119
|
+
config: 可选配置字典,写入临时 yml;未提供则用入口类所在目录的 application.yml。
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, main_class: Type, config: Optional[Dict[str, Any]] = None):
|
|
123
|
+
from spring.context.application_context import ApplicationContext
|
|
124
|
+
from spring.config.config_loader import ConfigLoader
|
|
125
|
+
|
|
126
|
+
self._temp_config_path: Optional[str] = None
|
|
127
|
+
if config is not None:
|
|
128
|
+
self._temp_config_path = _write_temp_config(config)
|
|
129
|
+
loader = ConfigLoader(config_path=self._temp_config_path)
|
|
130
|
+
else:
|
|
131
|
+
loader = None
|
|
132
|
+
self._context = ApplicationContext(main_class, config_loader=loader)
|
|
133
|
+
self._context.refresh()
|
|
134
|
+
|
|
135
|
+
def get_context(self):
|
|
136
|
+
return self._context
|
|
137
|
+
|
|
138
|
+
def get_bean(self, name: str) -> Any:
|
|
139
|
+
return self._context.get_bean(name)
|
|
140
|
+
|
|
141
|
+
def get_bean_by_type(self, bean_type: Type) -> Any:
|
|
142
|
+
return self._context.get_bean_by_type(bean_type)
|
|
143
|
+
|
|
144
|
+
def publish_event(self, event: Any) -> Any:
|
|
145
|
+
return self._context.publish_event(event)
|
|
146
|
+
|
|
147
|
+
def close(self) -> None:
|
|
148
|
+
try:
|
|
149
|
+
self._context.destroy()
|
|
150
|
+
finally:
|
|
151
|
+
if self._temp_config_path and os.path.exists(self._temp_config_path):
|
|
152
|
+
os.unlink(self._temp_config_path)
|
|
153
|
+
|
|
154
|
+
def __enter__(self):
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
158
|
+
self.close()
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ==================== WebMvcTest(Web 切片)====================
|
|
163
|
+
|
|
164
|
+
class WebMvcTest:
|
|
165
|
+
"""Web 切片测试(对齐 ``@WebMvcTest``):只注册指定 Controller,依赖 Mock 注入。
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
controllers: 要注册的 Controller 类列表。
|
|
169
|
+
config: 可选配置字典。
|
|
170
|
+
mock_dependencies: 是否用 ``MagicMock`` 注入构造函数依赖(默认 True)。
|
|
171
|
+
controller_advice: 可选的 ``@ControllerAdvice`` 类列表,注册全局异常处理。
|
|
172
|
+
|
|
173
|
+
用法::
|
|
174
|
+
|
|
175
|
+
with WebMvcTest(controllers=[UserController]) as mvc:
|
|
176
|
+
resp = mvc.get_client().get("/users")
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
controllers: List[Type],
|
|
182
|
+
config: Optional[Dict[str, Any]] = None,
|
|
183
|
+
mock_dependencies: bool = True,
|
|
184
|
+
controller_advice: Optional[List[Type]] = None,
|
|
185
|
+
):
|
|
186
|
+
from spring.context.application_context import ApplicationContext
|
|
187
|
+
from spring.context.bean_definition import BeanDefinition
|
|
188
|
+
from spring.config.config_loader import ConfigLoader
|
|
189
|
+
from spring.web.web_context import WebApplicationContext
|
|
190
|
+
|
|
191
|
+
if not controllers:
|
|
192
|
+
raise ValueError("controllers 不能为空")
|
|
193
|
+
|
|
194
|
+
self._temp_config_path: Optional[str] = None
|
|
195
|
+
loader = ConfigLoader(
|
|
196
|
+
config_path=(_write_temp_config(config) if config is not None else _write_temp_config({}))
|
|
197
|
+
)
|
|
198
|
+
self._temp_config_path = loader.config_path
|
|
199
|
+
# 构造一个不 refresh 的最小上下文,手动注册 Controller Bean
|
|
200
|
+
self._context = ApplicationContext(_MinimalApp, config_loader=loader)
|
|
201
|
+
|
|
202
|
+
for ctrl_cls in controllers:
|
|
203
|
+
instance = _instantiate_with_mocks(ctrl_cls) if mock_dependencies else ctrl_cls()
|
|
204
|
+
bean_name = self._generate_bean_name(ctrl_cls)
|
|
205
|
+
definition = BeanDefinition(bean_class=ctrl_cls, bean_name=bean_name)
|
|
206
|
+
# 复制类上的注解到定义(@RestController 等)
|
|
207
|
+
for ann in getattr(ctrl_cls, '__spring_annotations__', []):
|
|
208
|
+
definition.add_annotation(ann)
|
|
209
|
+
self._context.bean_factory.register_bean_definition(bean_name, definition)
|
|
210
|
+
self._context.bean_factory.register_instance(bean_name, instance)
|
|
211
|
+
|
|
212
|
+
# 注册 ControllerAdvice(全局异常处理)
|
|
213
|
+
for advice_cls in (controller_advice or []):
|
|
214
|
+
advice_instance = advice_cls()
|
|
215
|
+
advice_name = self._generate_bean_name(advice_cls)
|
|
216
|
+
advice_def = BeanDefinition(bean_class=advice_cls, bean_name=advice_name)
|
|
217
|
+
for ann in getattr(advice_cls, '__spring_annotations__', []):
|
|
218
|
+
advice_def.add_annotation(ann)
|
|
219
|
+
self._context.bean_factory.register_bean_definition(advice_name, advice_def)
|
|
220
|
+
self._context.bean_factory.register_instance(advice_name, advice_instance)
|
|
221
|
+
|
|
222
|
+
# 构建 WebApplicationContext 并初始化路由
|
|
223
|
+
self._web_context = WebApplicationContext(self._context)
|
|
224
|
+
self._web_context.init()
|
|
225
|
+
self._client = None
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def _generate_bean_name(cls: Type) -> str:
|
|
229
|
+
name = cls.__name__
|
|
230
|
+
return name[0].lower() + name[1:] if name else name
|
|
231
|
+
|
|
232
|
+
def get_app(self):
|
|
233
|
+
return self._web_context.get_app()
|
|
234
|
+
|
|
235
|
+
def get_context(self):
|
|
236
|
+
return self._context
|
|
237
|
+
|
|
238
|
+
def get_client(self):
|
|
239
|
+
from fastapi.testclient import TestClient
|
|
240
|
+
if self._client is None:
|
|
241
|
+
self._client = TestClient(self.get_app())
|
|
242
|
+
return self._client
|
|
243
|
+
|
|
244
|
+
def get_controller(self, ctrl_cls: Type) -> Any:
|
|
245
|
+
return self._context.get_bean_by_type(ctrl_cls)
|
|
246
|
+
|
|
247
|
+
def close(self) -> None:
|
|
248
|
+
try:
|
|
249
|
+
self._context.destroy()
|
|
250
|
+
finally:
|
|
251
|
+
if self._temp_config_path and os.path.exists(self._temp_config_path):
|
|
252
|
+
os.unlink(self._temp_config_path)
|
|
253
|
+
|
|
254
|
+
def __enter__(self):
|
|
255
|
+
return self
|
|
256
|
+
|
|
257
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
258
|
+
self.close()
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ==================== DataJpaTest(数据切片)====================
|
|
263
|
+
|
|
264
|
+
class DataJpaTest:
|
|
265
|
+
"""数据切片测试(对齐 ``@DataJpaTest``):内存 SQLite + 建表 + Repository 工厂。
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
entities: 实体类列表(``@entity`` 标注)。
|
|
269
|
+
dialect: 数据库方言(默认 ``sqlite``,内存库)。
|
|
270
|
+
|
|
271
|
+
用法::
|
|
272
|
+
|
|
273
|
+
with DataJpaTest(entities=[User]) as jpa:
|
|
274
|
+
repo = jpa.repository_for(User)
|
|
275
|
+
repo.save(User(name="tom"))
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
def __init__(self, entities: List[Type], dialect: str = "sqlite"):
|
|
279
|
+
if not entities:
|
|
280
|
+
raise ValueError("entities 不能为空")
|
|
281
|
+
from spring.orm.ddl_auto import DdlAutoManager
|
|
282
|
+
|
|
283
|
+
self._entities = list(entities)
|
|
284
|
+
self._dialect = dialect
|
|
285
|
+
self._conn = sqlite3.connect(":memory:")
|
|
286
|
+
self._conn.row_factory = None
|
|
287
|
+
self._pool = TestPool(self._conn)
|
|
288
|
+
self._mgr = DdlAutoManager(self._pool, dialect=dialect, mode="create")
|
|
289
|
+
for entity_cls in self._entities:
|
|
290
|
+
self._mgr.register_entity(entity_cls)
|
|
291
|
+
self._mgr.execute()
|
|
292
|
+
|
|
293
|
+
def get_connection(self):
|
|
294
|
+
return self._conn
|
|
295
|
+
|
|
296
|
+
def get_pool(self) -> TestPool:
|
|
297
|
+
return self._pool
|
|
298
|
+
|
|
299
|
+
def repository_for(self, entity_class: Type):
|
|
300
|
+
"""为指定实体构造 ``PagingAndSortingRepository``(复用 Spring Data 抽象)。"""
|
|
301
|
+
from spring.data import PagingAndSortingRepository
|
|
302
|
+
return PagingAndSortingRepository(self._pool, entity_class, dialect=self._dialect)
|
|
303
|
+
|
|
304
|
+
def get_entities(self) -> List[Type]:
|
|
305
|
+
return list(self._entities)
|
|
306
|
+
|
|
307
|
+
def close(self) -> None:
|
|
308
|
+
try:
|
|
309
|
+
self._conn.close()
|
|
310
|
+
except Exception:
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
def __enter__(self):
|
|
314
|
+
return self
|
|
315
|
+
|
|
316
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
317
|
+
self.close()
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# ==================== 最小入口类 ====================
|
|
322
|
+
|
|
323
|
+
class _MinimalApp:
|
|
324
|
+
"""``WebMvcTest`` 用的最小 ``@SpringBootApplication`` 入口(空扫描基包)。"""
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# 应用 @SpringBootApplication 注解到最小入口,满足 ApplicationContext 构造
|
|
329
|
+
try:
|
|
330
|
+
from spring.annotations.core import SpringBootApplication
|
|
331
|
+
_MinimalApp = SpringBootApplication(scan_base_packages=[])(_MinimalApp)
|
|
332
|
+
except ImportError: # pragma: no cover
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
__all__ = [
|
|
337
|
+
"TestPool",
|
|
338
|
+
"SpringBootTest",
|
|
339
|
+
"WebMvcTest",
|
|
340
|
+
"DataJpaTest",
|
|
341
|
+
]
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SkyWalking分布式追踪模块
|
|
3
|
+
集成SkyWalking实现分布式链路追踪
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("Spring.Tracing.SkyWalking")
|
|
12
|
+
|
|
13
|
+
# 可选导入SkyWalking
|
|
14
|
+
try:
|
|
15
|
+
from skywalking import agent, config
|
|
16
|
+
from skywalking.trace import context, tag
|
|
17
|
+
from skywalking.trace.carrier import Carrier
|
|
18
|
+
_skywalking_available = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
agent = None
|
|
21
|
+
config = None
|
|
22
|
+
context = None
|
|
23
|
+
tag = None
|
|
24
|
+
Carrier = None
|
|
25
|
+
_skywalking_available = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SkyWalkingTracer:
|
|
29
|
+
"""SkyWalking追踪器"""
|
|
30
|
+
|
|
31
|
+
_instance = None
|
|
32
|
+
_lock = threading.Lock()
|
|
33
|
+
|
|
34
|
+
def __new__(cls, *args, **kwargs):
|
|
35
|
+
if cls._instance is None:
|
|
36
|
+
with cls._lock:
|
|
37
|
+
if cls._instance is None:
|
|
38
|
+
cls._instance = super().__new__(cls)
|
|
39
|
+
return cls._instance
|
|
40
|
+
|
|
41
|
+
def __init__(self, service_name: str = "spring-python-app",
|
|
42
|
+
collector_address: str = "127.0.0.1:11800"):
|
|
43
|
+
if hasattr(self, '_initialized'):
|
|
44
|
+
return
|
|
45
|
+
self.service_name = service_name
|
|
46
|
+
self.collector_address = collector_address
|
|
47
|
+
self._initialized = False
|
|
48
|
+
self._local_context = threading.local()
|
|
49
|
+
|
|
50
|
+
# 如果SkyWalking可用,尝试初始化
|
|
51
|
+
if _skywalking_available:
|
|
52
|
+
self._init_skywalking()
|
|
53
|
+
|
|
54
|
+
def _init_skywalking(self):
|
|
55
|
+
"""初始化SkyWalking Agent"""
|
|
56
|
+
try:
|
|
57
|
+
# 配置SkyWalking
|
|
58
|
+
config.service_name = self.service_name
|
|
59
|
+
config.collector_address = self.collector_address
|
|
60
|
+
config.protocol = 'grpc'
|
|
61
|
+
|
|
62
|
+
# 启动SkyWalking Agent
|
|
63
|
+
agent.start()
|
|
64
|
+
|
|
65
|
+
self._initialized = True
|
|
66
|
+
logger.info(f"[SkyWalking] Agent started, service: {self.service_name}, collector: {self.collector_address}")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.warning(f"[SkyWalking] Failed to initialize: {e}. Falling back to local tracing.")
|
|
69
|
+
self._initialized = False
|
|
70
|
+
|
|
71
|
+
def create_span(self, operation_name: str, span_type: str = "Local",
|
|
72
|
+
peer: str = "") -> 'Span':
|
|
73
|
+
"""
|
|
74
|
+
创建Span
|
|
75
|
+
|
|
76
|
+
参数:
|
|
77
|
+
operation_name: 操作名称
|
|
78
|
+
span_type: Span类型(Local/Remote/DB/MQ等)
|
|
79
|
+
peer: 对端地址
|
|
80
|
+
|
|
81
|
+
返回:
|
|
82
|
+
Span对象
|
|
83
|
+
"""
|
|
84
|
+
if _skywalking_available and self._initialized:
|
|
85
|
+
# 使用SkyWalking创建Span
|
|
86
|
+
try:
|
|
87
|
+
span = context.create_entry_span(operation_name, Carrier())
|
|
88
|
+
span.tag(tag.Tag(key="span.type", val=span_type))
|
|
89
|
+
if peer:
|
|
90
|
+
span.tag(tag.Tag(key="peer", val=peer))
|
|
91
|
+
return span
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.warning(f"[SkyWalking] Failed to create span: {e}")
|
|
94
|
+
|
|
95
|
+
# 回退到本地Span
|
|
96
|
+
return LocalSpan(operation_name, span_type, peer)
|
|
97
|
+
|
|
98
|
+
def create_exit_span(self, operation_name: str, peer: str) -> 'Span':
|
|
99
|
+
"""
|
|
100
|
+
创建Exit Span(调用外部服务)
|
|
101
|
+
|
|
102
|
+
参数:
|
|
103
|
+
operation_name: 操作名称
|
|
104
|
+
peer: 对端地址
|
|
105
|
+
|
|
106
|
+
返回:
|
|
107
|
+
Span对象
|
|
108
|
+
"""
|
|
109
|
+
if _skywalking_available and self._initialized:
|
|
110
|
+
try:
|
|
111
|
+
span = context.create_exit_span(operation_name, peer, Carrier())
|
|
112
|
+
return span
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.warning(f"[SkyWalking] Failed to create exit span: {e}")
|
|
115
|
+
|
|
116
|
+
# 回退到本地Span
|
|
117
|
+
return LocalSpan(operation_name, "Remote", peer)
|
|
118
|
+
|
|
119
|
+
def get_trace_id(self) -> str:
|
|
120
|
+
"""获取当前Trace ID"""
|
|
121
|
+
if _skywalking_available and self._initialized:
|
|
122
|
+
try:
|
|
123
|
+
active_span = context.get_active_span()
|
|
124
|
+
if active_span:
|
|
125
|
+
return str(active_span.trace_id)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.warning(f"[SkyWalking] Failed to get trace ID: {e}")
|
|
128
|
+
|
|
129
|
+
# 回退到本地Trace ID
|
|
130
|
+
return getattr(self._local_context, 'trace_id', "")
|
|
131
|
+
|
|
132
|
+
def set_trace_id(self, trace_id: str):
|
|
133
|
+
"""设置当前Trace ID"""
|
|
134
|
+
self._local_context.trace_id = trace_id
|
|
135
|
+
|
|
136
|
+
def inject_carrier(self, headers: Dict[str, str]) -> Dict[str, str]:
|
|
137
|
+
"""
|
|
138
|
+
将Trace信息注入到请求头中
|
|
139
|
+
|
|
140
|
+
参数:
|
|
141
|
+
headers: 请求头字典
|
|
142
|
+
|
|
143
|
+
返回:
|
|
144
|
+
包含Trace信息的请求头
|
|
145
|
+
"""
|
|
146
|
+
if _skywalking_available and self._initialized:
|
|
147
|
+
try:
|
|
148
|
+
carrier = Carrier()
|
|
149
|
+
context.inject(carrier)
|
|
150
|
+
for item in carrier:
|
|
151
|
+
headers[item.key] = item.val
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.warning(f"[SkyWalking] Failed to inject carrier: {e}")
|
|
154
|
+
|
|
155
|
+
return headers
|
|
156
|
+
|
|
157
|
+
def extract_carrier(self, headers: Dict[str, str]):
|
|
158
|
+
"""
|
|
159
|
+
从请求头中提取Trace信息
|
|
160
|
+
|
|
161
|
+
参数:
|
|
162
|
+
headers: 请求头字典
|
|
163
|
+
"""
|
|
164
|
+
if _skywalking_available and self._initialized:
|
|
165
|
+
try:
|
|
166
|
+
carrier = Carrier()
|
|
167
|
+
for item in carrier:
|
|
168
|
+
if item.key in headers:
|
|
169
|
+
item.val = headers[item.key]
|
|
170
|
+
context.extract(carrier)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.warning(f"[SkyWalking] Failed to extract carrier: {e}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class LocalSpan:
|
|
176
|
+
"""本地Span(SkyWalking不可用时的回退实现)"""
|
|
177
|
+
|
|
178
|
+
def __init__(self, operation_name: str, span_type: str, peer: str = ""):
|
|
179
|
+
self.operation_name = operation_name
|
|
180
|
+
self.span_type = span_type
|
|
181
|
+
self.peer = peer
|
|
182
|
+
self.start_time = time.time()
|
|
183
|
+
self.end_time = None
|
|
184
|
+
self.tags = {}
|
|
185
|
+
self.trace_id = str(uuid.uuid4())[:16]
|
|
186
|
+
|
|
187
|
+
logger.info(f"[LocalTrace] Start span={operation_name}, trace_id={self.trace_id}, type={span_type}")
|
|
188
|
+
|
|
189
|
+
def tag(self, tag_obj):
|
|
190
|
+
"""添加标签"""
|
|
191
|
+
if hasattr(tag_obj, 'key') and hasattr(tag_obj, 'val'):
|
|
192
|
+
self.tags[tag_obj.key] = tag_obj.val
|
|
193
|
+
elif isinstance(tag_obj, tuple):
|
|
194
|
+
self.tags[tag_obj[0]] = tag_obj[1]
|
|
195
|
+
|
|
196
|
+
def finish(self):
|
|
197
|
+
"""结束Span"""
|
|
198
|
+
self.end_time = time.time()
|
|
199
|
+
duration = (self.end_time - self.start_time) * 1000
|
|
200
|
+
logger.info(
|
|
201
|
+
f"[LocalTrace] End span={self.operation_name}, trace_id={self.trace_id}, "
|
|
202
|
+
f"duration={duration:.2f}ms, type={self.span_type}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def __enter__(self):
|
|
206
|
+
return self
|
|
207
|
+
|
|
208
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
209
|
+
self.finish()
|
|
210
|
+
if exc_val:
|
|
211
|
+
logger.error(f"[LocalTrace] Error span={self.operation_name}, error={exc_val}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# 创建全局SkyWalking追踪器实例
|
|
215
|
+
skywalking_tracer = SkyWalkingTracer()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def init_skywalking(config: dict) -> None:
|
|
219
|
+
"""
|
|
220
|
+
初始化SkyWalking配置
|
|
221
|
+
|
|
222
|
+
参数:
|
|
223
|
+
config: 配置字典,包含service_name, collector_address等
|
|
224
|
+
"""
|
|
225
|
+
global skywalking_tracer
|
|
226
|
+
skywalking_tracer = SkyWalkingTracer(
|
|
227
|
+
service_name=config.get('service_name', 'spring-python-app'),
|
|
228
|
+
collector_address=config.get('collector_address', '127.0.0.1:11800')
|
|
229
|
+
)
|
spring/tx/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""SpringBootAI 事务扩展模块(对齐 Spring ``@TransactionalEventListener``)。
|
|
2
|
+
|
|
3
|
+
模块组成:
|
|
4
|
+
- ``synchronization``: ``TransactionSynchronizationManager`` + ``TransactionSynchronization`` +
|
|
5
|
+
``TransactionPhase`` —— 事务同步回调管理(``ContextVar`` 兼容协程)。
|
|
6
|
+
- ``events``: ``@TransactionalEventListener`` 注解 + ``TransactionalEventPublisher`` ——
|
|
7
|
+
事务阶段事件监听。
|
|
8
|
+
|
|
9
|
+
典型用法::
|
|
10
|
+
|
|
11
|
+
from spring.tx import (
|
|
12
|
+
TransactionalEventListener, TransactionPhase,
|
|
13
|
+
TransactionalEventPublisher, TransactionSynchronizationManager,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
class OrderCreatedEvent(ApplicationEvent):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
class OrderService:
|
|
20
|
+
@TransactionalEventListener(phase=TransactionPhase.AFTER_COMMIT)
|
|
21
|
+
def on_order_created(self, event: OrderCreatedEvent):
|
|
22
|
+
... # 事务提交后才执行
|
|
23
|
+
|
|
24
|
+
集成:
|
|
25
|
+
- ``@Transactional`` 切面(``bean_factory._wrap_transactional``)在事务边界调用
|
|
26
|
+
``TransactionSynchronizationManager.init/clear`` 与各 ``trigger_*`` 触发回调。
|
|
27
|
+
- ``ApplicationContext`` 扫描 ``@TransactionalEventListener`` 注册到
|
|
28
|
+
``TransactionalEventPublisher``,``publish_event`` 委托触发。
|
|
29
|
+
|
|
30
|
+
与 Java 的差异:
|
|
31
|
+
- 用 ``ContextVar`` 替代 ``ThreadLocal``,兼容 ``asyncio`` 协程。
|
|
32
|
+
- 同步回调抛错统一记录不中断事务(Spring ``beforeCommit`` 抛错会回滚),已在文档标注。
|
|
33
|
+
"""
|
|
34
|
+
from .synchronization import (
|
|
35
|
+
TransactionPhase,
|
|
36
|
+
TransactionSynchronization,
|
|
37
|
+
TransactionSynchronizationManager,
|
|
38
|
+
transaction_sync_scope,
|
|
39
|
+
)
|
|
40
|
+
from .events import TransactionalEventListener, TransactionalEventPublisher
|
|
41
|
+
|
|
42
|
+
__version__ = "1.0.0"
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"TransactionPhase",
|
|
46
|
+
"TransactionSynchronization",
|
|
47
|
+
"TransactionSynchronizationManager",
|
|
48
|
+
"transaction_sync_scope",
|
|
49
|
+
"TransactionalEventListener",
|
|
50
|
+
"TransactionalEventPublisher",
|
|
51
|
+
"__version__",
|
|
52
|
+
]
|