springbootAI 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. spring/__init__.py +66 -0
  2. spring/ai/__init__.py +78 -0
  3. spring/ai/advisors.py +139 -0
  4. spring/ai/annotations.py +74 -0
  5. spring/ai/autoconfig.py +481 -0
  6. spring/ai/core.py +391 -0
  7. spring/ai/etl.py +188 -0
  8. spring/ai/memory.py +109 -0
  9. spring/ai/observability.py +129 -0
  10. spring/ai/providers.py +789 -0
  11. spring/ai/resilience.py +258 -0
  12. spring/ai/tools.py +106 -0
  13. spring/ai/vectorstore.py +303 -0
  14. spring/annotations/__init__.py +188 -0
  15. spring/annotations/cache.py +126 -0
  16. spring/annotations/cloud.py +207 -0
  17. spring/annotations/conditional.py +272 -0
  18. spring/annotations/core.py +864 -0
  19. spring/annotations/messaging.py +107 -0
  20. spring/aop/__init__.py +4 -0
  21. spring/aop/cloud_aop.py +404 -0
  22. spring/aop/comprehensive_aop.py +1015 -0
  23. spring/aop/method_interceptor.py +19 -0
  24. spring/aop/proxy_factory.py +55 -0
  25. spring/cloud/__init__.py +76 -0
  26. spring/cloud/discovery.py +364 -0
  27. spring/cloud/feign.py +469 -0
  28. spring/cloud/gateway.py +452 -0
  29. spring/cloud/load_balancer.py +149 -0
  30. spring/cloud/seata.py +557 -0
  31. spring/cloud/sentinel.py +525 -0
  32. spring/cloud/tracer.py +337 -0
  33. spring/config/__init__.py +21 -0
  34. spring/config/binding.py +206 -0
  35. spring/config/config_loader.py +405 -0
  36. spring/context/__init__.py +13 -0
  37. spring/context/application_context.py +589 -0
  38. spring/context/bean_definition.py +70 -0
  39. spring/context/bean_factory.py +1052 -0
  40. spring/context/registry.py +58 -0
  41. spring/context/scanner.py +106 -0
  42. spring/core/__init__.py +3 -0
  43. spring/core/graceful_shutdown.py +196 -0
  44. spring/core/typing_utils.py +50 -0
  45. spring/csv/__init__.py +52 -0
  46. spring/csv/annotations.py +402 -0
  47. spring/csv/converters.py +69 -0
  48. spring/csv/easy_csv.py +95 -0
  49. spring/csv/exceptions.py +27 -0
  50. spring/csv/reader.py +195 -0
  51. spring/csv/writer.py +155 -0
  52. spring/data/__init__.py +54 -0
  53. spring/data/page.py +181 -0
  54. spring/data/repository.py +274 -0
  55. spring/data/specification.py +228 -0
  56. spring/datasource/__init__.py +66 -0
  57. spring/datasource/annotations.py +133 -0
  58. spring/datasource/context.py +69 -0
  59. spring/datasource/dynamic.py +148 -0
  60. spring/event/__init__.py +7 -0
  61. spring/event/publisher.py +69 -0
  62. spring/excel/__init__.py +51 -0
  63. spring/excel/annotations.py +405 -0
  64. spring/excel/converters.py +231 -0
  65. spring/excel/easy_excel.py +94 -0
  66. spring/excel/exceptions.py +31 -0
  67. spring/excel/reader.py +254 -0
  68. spring/excel/style.py +95 -0
  69. spring/excel/writer.py +197 -0
  70. spring/i18n/__init__.py +97 -0
  71. spring/i18n/accessor.py +94 -0
  72. spring/i18n/auto_config.py +177 -0
  73. spring/i18n/holder.py +106 -0
  74. spring/i18n/locale.py +152 -0
  75. spring/i18n/locale_resolver.py +367 -0
  76. spring/i18n/message_source.py +250 -0
  77. spring/i18n/middleware.py +79 -0
  78. spring/i18n/properties.py +168 -0
  79. spring/i18n/sources.py +255 -0
  80. spring/logging/__init__.py +1 -0
  81. spring/logging/loguru_logger.py +228 -0
  82. spring/main.py +378 -0
  83. spring/messaging/__init__.py +1 -0
  84. spring/messaging/rabbitmq.py +302 -0
  85. spring/monitoring/__init__.py +1 -0
  86. spring/monitoring/prometheus.py +199 -0
  87. spring/orm/__init__.py +258 -0
  88. spring/orm/database.py +222 -0
  89. spring/orm/ddl_auto.py +1217 -0
  90. spring/orm/migration.py +419 -0
  91. spring/orm/mybatis_integration.py +400 -0
  92. spring/orm/pymybatis/__init__.py +86 -0
  93. spring/orm/pymybatis/annotations/__init__.py +30 -0
  94. spring/orm/pymybatis/annotations/annotations.py +332 -0
  95. spring/orm/pymybatis/cache/__init__.py +47 -0
  96. spring/orm/pymybatis/cache/cache.py +371 -0
  97. spring/orm/pymybatis/cache/redis_cache.py +434 -0
  98. spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
  99. spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
  100. spring/orm/pymybatis/configuration.py +525 -0
  101. spring/orm/pymybatis/core/__init__.py +10 -0
  102. spring/orm/pymybatis/core/sql_session.py +1382 -0
  103. spring/orm/pymybatis/core/sql_session_factory.py +76 -0
  104. spring/orm/pymybatis/dialect/__init__.py +9 -0
  105. spring/orm/pymybatis/dialect/dialect.py +445 -0
  106. spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
  107. spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
  108. spring/orm/pymybatis/interceptor/__init__.py +31 -0
  109. spring/orm/pymybatis/interceptor/interceptor.py +427 -0
  110. spring/orm/pymybatis/mapper/__init__.py +9 -0
  111. spring/orm/pymybatis/mapper/mapper.py +540 -0
  112. spring/orm/pymybatis/metrics/__init__.py +41 -0
  113. spring/orm/pymybatis/metrics/metrics.py +595 -0
  114. spring/orm/pymybatis/pool/__init__.py +9 -0
  115. spring/orm/pymybatis/pool/connection_pool.py +711 -0
  116. spring/orm/pymybatis/security/__init__.py +19 -0
  117. spring/orm/pymybatis/security/access_control.py +415 -0
  118. spring/orm/pymybatis/security/password_encoder.py +293 -0
  119. spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
  120. spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
  121. spring/orm/pymybatis/transaction/__init__.py +9 -0
  122. spring/orm/pymybatis/transaction/transaction.py +288 -0
  123. spring/orm/pymybatis/type_handler/__init__.py +37 -0
  124. spring/orm/pymybatis/type_handler/type_handler.py +473 -0
  125. spring/orm/pymybatis/version.py +9 -0
  126. spring/orm/pymybatis/xml_parser/__init__.py +9 -0
  127. spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
  128. spring/retry/__init__.py +12 -0
  129. spring/retry/retry_annotations.py +71 -0
  130. spring/retry/retry_decorator.py +155 -0
  131. spring/scheduling/__init__.py +3 -0
  132. spring/scheduling/scheduler.py +389 -0
  133. spring/security/__init__.py +39 -0
  134. spring/security/jwt_utils.py +281 -0
  135. spring/security/replay_protection.py +206 -0
  136. spring/security/secret_manager.py +226 -0
  137. spring/security/security_aop.py +248 -0
  138. spring/security/security_context.py +172 -0
  139. spring/test/__init__.py +45 -0
  140. spring/test/slicing.py +341 -0
  141. spring/tracing/__init__.py +11 -0
  142. spring/tracing/skywalking.py +229 -0
  143. spring/tx/__init__.py +52 -0
  144. spring/tx/events.py +172 -0
  145. spring/tx/synchronization.py +143 -0
  146. spring/utils/__init__.py +5 -0
  147. spring/utils/banner.py +32 -0
  148. spring/utils/logger.py +73 -0
  149. spring/utils/redis_client.py +526 -0
  150. spring/validation/__init__.py +55 -0
  151. spring/validation/aop.py +141 -0
  152. spring/validation/constraints.py +357 -0
  153. spring/validation/exceptions.py +55 -0
  154. spring/validation/validator.py +139 -0
  155. spring/web/__init__.py +12 -0
  156. spring/web/actuator.py +319 -0
  157. spring/web/exception_handler.py +61 -0
  158. spring/web/health.py +399 -0
  159. spring/web/interceptor.py +91 -0
  160. spring/web/result.py +44 -0
  161. spring/web/swagger.py +601 -0
  162. spring/web/web_context.py +755 -0
  163. spring/websocket/__init__.py +86 -0
  164. spring/websocket/annotations.py +169 -0
  165. spring/websocket/broker.py +238 -0
  166. spring/websocket/exceptions.py +26 -0
  167. spring/websocket/handler.py +243 -0
  168. spring/websocket/router.py +526 -0
  169. spring/websocket/session.py +216 -0
  170. springbootai-1.8.0.dist-info/METADATA +2796 -0
  171. springbootai-1.8.0.dist-info/RECORD +175 -0
  172. springbootai-1.8.0.dist-info/WHEEL +5 -0
  173. springbootai-1.8.0.dist-info/entry_points.txt +2 -0
  174. springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
  175. springbootai-1.8.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,526 @@
1
+ """
2
+ Redis客户端工具类
3
+ 提供分布式锁、持久化存储等企业级功能
4
+ """
5
+ import json
6
+ import time
7
+ import uuid
8
+ from typing import Any, Optional
9
+
10
+ try:
11
+ from redis.exceptions import RedisError
12
+ except ImportError:
13
+ class RedisError(Exception):
14
+ """Fallback used when the optional Redis dependency is unavailable."""
15
+
16
+
17
+ class RedisClient:
18
+ """Redis客户端封装"""
19
+
20
+ def __init__(self, host: str = 'localhost', port: int = 6379, db: int = 0, password: str = None):
21
+ self._client = None
22
+ self.configure(host=host, port=port, db=db, password=password)
23
+
24
+ def configure(self, host: str, port: int, db: int, password: str = None) -> None:
25
+ self.host = host
26
+ self.port = int(port)
27
+ self.db = int(db)
28
+ self.password = password
29
+ self._client = None
30
+
31
+ def connect(self, strict: bool = False) -> None:
32
+ """连接Redis"""
33
+ try:
34
+ from redis import Redis
35
+
36
+ self._client = Redis(
37
+ host=self.host,
38
+ port=self.port,
39
+ db=self.db,
40
+ password=self.password,
41
+ decode_responses=True,
42
+ socket_timeout=5,
43
+ socket_connect_timeout=5
44
+ )
45
+ # 测试连接
46
+ self._client.ping()
47
+ except ImportError as exc:
48
+ self._client = None
49
+ if strict:
50
+ raise RuntimeError("Redis已启用但redis依赖未安装") from exc
51
+ except Exception as e:
52
+ self._client = None
53
+ if strict:
54
+ raise ConnectionError(f"无法连接Redis: {e}") from e
55
+
56
+ def get_client(self):
57
+ """获取Redis客户端"""
58
+ if self._client is None:
59
+ self.connect()
60
+ return self._client
61
+
62
+ # ==================== 分布式锁 ====================
63
+
64
+ def acquire_lock(self, key: str, timeout: int = 10, wait_timeout: int = 5):
65
+ """
66
+ 获取分布式锁
67
+
68
+ Args:
69
+ key: 锁键
70
+ timeout: 锁过期时间(秒)
71
+ wait_timeout: 等待锁的超时时间(秒)
72
+
73
+ Returns:
74
+ 锁标识(用于释放锁),获取失败返回None
75
+ """
76
+ client = self.get_client()
77
+ if client is None:
78
+ return None
79
+
80
+ lock_id = str(uuid.uuid4())
81
+ end_time = time.time() + wait_timeout
82
+
83
+ while time.time() < end_time:
84
+ # 使用SET NX EX命令获取锁
85
+ result = client.set(f"lock:{key}", lock_id, nx=True, ex=timeout)
86
+ if result:
87
+ return lock_id
88
+ time.sleep(0.01) # 短暂等待后重试
89
+
90
+ return None
91
+
92
+ def release_lock(self, key: str, lock_id: str) -> bool:
93
+ """
94
+ 释放分布式锁
95
+
96
+ Args:
97
+ key: 锁键
98
+ lock_id: 锁标识
99
+
100
+ Returns:
101
+ 是否成功释放
102
+ """
103
+ client = self.get_client()
104
+ if client is None:
105
+ return False
106
+
107
+ # 使用Lua脚本保证原子性释放
108
+ script = """
109
+ if redis.call("get", KEYS[1]) == ARGV[1] then
110
+ return redis.call("del", KEYS[1])
111
+ else
112
+ return 0
113
+ end
114
+ """
115
+ result = client.eval(script, 1, f"lock:{key}", lock_id)
116
+ return result == 1
117
+
118
+ # ==================== 持久化存储 ====================
119
+
120
+ def set_value(self, key: str, value: Any, expire: int = None) -> bool:
121
+ """
122
+ 设置值
123
+
124
+ Args:
125
+ key: 键
126
+ value: 值(支持任意可JSON序列化的类型)
127
+ expire: 过期时间(秒)
128
+
129
+ Returns:
130
+ 是否成功
131
+ """
132
+ client = self.get_client()
133
+ if client is None:
134
+ return False
135
+
136
+ try:
137
+ if isinstance(value, (str, int, float, bool)):
138
+ result = client.set(key, value, ex=expire)
139
+ else:
140
+ result = client.set(key, json.dumps(value), ex=expire)
141
+ return result is not None
142
+ except (RedisError, TypeError):
143
+ return False
144
+
145
+ def get_value(self, key: str) -> Any:
146
+ """
147
+ 获取值
148
+
149
+ Args:
150
+ key: 键
151
+
152
+ Returns:
153
+ 值(自动反序列化)
154
+ """
155
+ client = self.get_client()
156
+ if client is None:
157
+ return None
158
+
159
+ try:
160
+ value = client.get(key)
161
+ if value is None:
162
+ return None
163
+
164
+ # 尝试解析为JSON
165
+ try:
166
+ return json.loads(value)
167
+ except (json.JSONDecodeError, TypeError):
168
+ return value
169
+ except RedisError:
170
+ return None
171
+
172
+ def delete_key(self, key: str) -> bool:
173
+ """
174
+ 删除键
175
+
176
+ Args:
177
+ key: 键
178
+
179
+ Returns:
180
+ 是否成功
181
+ """
182
+ client = self.get_client()
183
+ if client is None:
184
+ return False
185
+
186
+ try:
187
+ result = client.delete(key)
188
+ return result > 0
189
+ except RedisError:
190
+ return False
191
+
192
+ def exists_key(self, key: str) -> bool:
193
+ """
194
+ 检查键是否存在
195
+
196
+ Args:
197
+ key: 键
198
+
199
+ Returns:
200
+ 是否存在
201
+ """
202
+ client = self.get_client()
203
+ if client is None:
204
+ return False
205
+
206
+ try:
207
+ return client.exists(key) > 0
208
+ except RedisError:
209
+ return False
210
+
211
+ # ==================== 列表操作 ====================
212
+
213
+ def list_push(self, key: str, value: Any) -> int:
214
+ """
215
+ 向列表尾部添加元素
216
+
217
+ Args:
218
+ key: 键
219
+ value: 值
220
+
221
+ Returns:
222
+ 列表长度
223
+ """
224
+ client = self.get_client()
225
+ if client is None:
226
+ return 0
227
+
228
+ try:
229
+ if not isinstance(value, str):
230
+ value = json.dumps(value)
231
+ return client.rpush(key, value)
232
+ except (RedisError, TypeError):
233
+ return 0
234
+
235
+ def list_range(self, key: str, start: int = 0, end: int = -1) -> list:
236
+ """
237
+ 获取列表指定范围的元素
238
+
239
+ Args:
240
+ key: 键
241
+ start: 起始索引
242
+ end: 结束索引
243
+
244
+ Returns:
245
+ 元素列表
246
+ """
247
+ client = self.get_client()
248
+ if client is None:
249
+ return []
250
+
251
+ try:
252
+ values = client.lrange(key, start, end)
253
+ result = []
254
+ for v in values:
255
+ try:
256
+ result.append(json.loads(v))
257
+ except (json.JSONDecodeError, TypeError):
258
+ result.append(v)
259
+ return result
260
+ except RedisError:
261
+ return []
262
+
263
+ def list_remove_range(self, key: str, start: int, end: int) -> int:
264
+ """
265
+ 删除列表指定范围的元素
266
+
267
+ Args:
268
+ key: 键
269
+ start: 起始索引
270
+ end: 结束索引
271
+
272
+ Returns:
273
+ 删除的元素数量
274
+ """
275
+ client = self.get_client()
276
+ if client is None:
277
+ return 0
278
+
279
+ try:
280
+ # 获取列表长度
281
+ length = client.llen(key)
282
+ if length == 0:
283
+ return 0
284
+
285
+ # 计算需要保留的元素
286
+ result = 0
287
+ # 删除从start到end的元素(通过截断实现)
288
+ if start > 0:
289
+ # 保留前start个元素
290
+ client.ltrim(key, 0, start - 1)
291
+ result = length - start
292
+ elif end < length - 1:
293
+ # 保留从end+1开始的元素
294
+ client.ltrim(key, end + 1, -1)
295
+ result = end + 1
296
+
297
+ return result
298
+ except RedisError:
299
+ return 0
300
+
301
+ def list_length(self, key: str) -> int:
302
+ """
303
+ 获取列表长度
304
+
305
+ Args:
306
+ key: 键
307
+
308
+ Returns:
309
+ 列表长度
310
+ """
311
+ client = self.get_client()
312
+ if client is None:
313
+ return 0
314
+
315
+ try:
316
+ return client.llen(key)
317
+ except RedisError:
318
+ return 0
319
+
320
+ # ==================== 计数器操作 ====================
321
+
322
+ def increment(self, key: str, amount: int = 1) -> int:
323
+ """
324
+ 递增计数器
325
+
326
+ Args:
327
+ key: 键
328
+ amount: 递增值
329
+
330
+ Returns:
331
+ 递增后的值
332
+ """
333
+ client = self.get_client()
334
+ if client is None:
335
+ return 0
336
+
337
+ try:
338
+ return client.incrby(key, amount)
339
+ except RedisError:
340
+ return 0
341
+
342
+ def decrement(self, key: str, amount: int = 1) -> int:
343
+ """
344
+ 递减计数器
345
+
346
+ Args:
347
+ key: 键
348
+ amount: 递减值
349
+
350
+ Returns:
351
+ 递减后的值
352
+ """
353
+ client = self.get_client()
354
+ if client is None:
355
+ return 0
356
+
357
+ try:
358
+ return client.decrby(key, amount)
359
+ except RedisError:
360
+ return 0
361
+
362
+ # ==================== Hash操作 ====================
363
+
364
+ def hash_set(self, key: str, field: str, value: Any) -> bool:
365
+ """
366
+ 设置Hash字段值
367
+
368
+ Args:
369
+ key: 键
370
+ field: 字段名
371
+ value: 值
372
+
373
+ Returns:
374
+ 是否成功
375
+ """
376
+ client = self.get_client()
377
+ if client is None:
378
+ return False
379
+
380
+ try:
381
+ if not isinstance(value, str):
382
+ value = json.dumps(value)
383
+ return client.hset(key, field, value) > 0
384
+ except (RedisError, TypeError):
385
+ return False
386
+
387
+ def hash_get(self, key: str, field: str) -> Any:
388
+ """
389
+ 获取Hash字段值
390
+
391
+ Args:
392
+ key: 键
393
+ field: 字段名
394
+
395
+ Returns:
396
+
397
+ """
398
+ client = self.get_client()
399
+ if client is None:
400
+ return None
401
+
402
+ try:
403
+ value = client.hget(key, field)
404
+ if value is None:
405
+ return None
406
+ try:
407
+ return json.loads(value)
408
+ except (json.JSONDecodeError, TypeError):
409
+ return value
410
+ except RedisError:
411
+ return None
412
+
413
+ def hash_get_all(self, key: str) -> dict:
414
+ """
415
+ 获取Hash所有字段和值
416
+
417
+ Args:
418
+ key: 键
419
+
420
+ Returns:
421
+ 字段-值字典
422
+ """
423
+ client = self.get_client()
424
+ if client is None:
425
+ return {}
426
+
427
+ try:
428
+ result = client.hgetall(key)
429
+ for field, value in list(result.items()):
430
+ try:
431
+ result[field] = json.loads(value)
432
+ except (json.JSONDecodeError, TypeError):
433
+ pass
434
+ return result
435
+ except RedisError:
436
+ return {}
437
+
438
+ def hash_delete(self, key: str, field: str) -> bool:
439
+ """
440
+ 删除Hash字段
441
+
442
+ Args:
443
+ key: 键
444
+ field: 字段名
445
+
446
+ Returns:
447
+ 是否成功
448
+ """
449
+ client = self.get_client()
450
+ if client is None:
451
+ return False
452
+
453
+ try:
454
+ return client.hdel(key, field) > 0
455
+ except RedisError:
456
+ return False
457
+
458
+ # ==================== 集合操作 ====================
459
+
460
+ def set_add(self, key: str, value: Any) -> bool:
461
+ """
462
+ 向集合添加元素
463
+
464
+ Args:
465
+ key: 键
466
+ value: 值
467
+
468
+ Returns:
469
+ 是否成功
470
+ """
471
+ client = self.get_client()
472
+ if client is None:
473
+ return False
474
+
475
+ try:
476
+ if not isinstance(value, str):
477
+ value = json.dumps(value)
478
+ return client.sadd(key, value) > 0
479
+ except (RedisError, TypeError):
480
+ return False
481
+
482
+ def set_members(self, key: str) -> set:
483
+ """
484
+ 获取集合所有元素
485
+
486
+ Args:
487
+ key: 键
488
+
489
+ Returns:
490
+ 元素集合
491
+ """
492
+ client = self.get_client()
493
+ if client is None:
494
+ return set()
495
+
496
+ try:
497
+ members = client.smembers(key)
498
+ result = set()
499
+ for m in members:
500
+ try:
501
+ result.add(json.loads(m))
502
+ except (json.JSONDecodeError, TypeError):
503
+ result.add(m)
504
+ return result
505
+ except RedisError:
506
+ return set()
507
+
508
+
509
+ # 创建全局Redis客户端实例
510
+ redis_client = RedisClient()
511
+
512
+
513
+ def init_redis(config: dict) -> None:
514
+ """
515
+ 初始化Redis连接
516
+
517
+ Args:
518
+ config: Redis配置字典,包含host, port, db, password等
519
+ """
520
+ redis_client.configure(
521
+ host=config.get('host', 'localhost'),
522
+ port=config.get('port', 6379),
523
+ db=config.get('db', 0),
524
+ password=config.get('password')
525
+ )
526
+ redis_client.connect(strict=True)
@@ -0,0 +1,55 @@
1
+ """SpringBootAI Bean Validation 模块 —— 字段约束注解 + 验证器 + 方法级 AOP。
2
+
3
+ 对齐 Jakarta Bean Validation(Hibernate Validator)的核心能力:
4
+ - 字段级约束(``@NotNull``/``@NotBlank``/``@Size``/``@Min``/``@Max``/``@Pattern``/``@Email`` 等)
5
+ 作为字段描述符,复用 ORM ``Column`` / Excel ``ExcelProperty`` 元数据范式。
6
+ - ``BeanValidator`` 反射收集约束并校验对象实例,返回 ``ConstraintViolation`` 列表。
7
+ - ``@BeanValidate`` 方法级 AOP 注解,接入既有 ``comprehensive_aop`` 分发链路,
8
+ 受管 Bean 方法调用前自动校验参数对象。
9
+
10
+ 模块组成:
11
+ - constraints: 字段约束注解(``Constraint`` 基类 + 14 个内置约束)
12
+ - validator: ``BeanValidator`` 反射校验器
13
+ - aop: ``@BeanValidate`` 方法级注解 + AOP 装饰器
14
+ - exceptions: ``ValidationError`` / ``ConstraintViolation``
15
+
16
+ 设计原则:**复用项目既有范式,不重复造轮子**。约束描述符、反射收集、AOP 注册全部对齐
17
+ 既有 ORM/Excel/综合 AOP 实现,未引入任何 Spring 风格第三方库。
18
+
19
+ 与 Java 的差异(已标注):
20
+ - 仅支持字段级约束(Java 还支持方法参数级/返回值级标量约束);方法级通过 ``@BeanValidate``
21
+ 对参数对象整体校验实现。
22
+ - 校验器为无状态静态方法风格,不依赖 IoC 容器即可独立使用。
23
+ """
24
+ from .exceptions import ConstraintViolation, ValidationError
25
+ from .constraints import (
26
+ Constraint,
27
+ NotNull, NotBlank, NotEmpty,
28
+ Size,
29
+ Min, Max,
30
+ Positive, PositiveOrZero, Negative, NegativeOrZero,
31
+ Pattern, Email,
32
+ AssertTrue, AssertFalse,
33
+ )
34
+ from .validator import BeanValidator
35
+ from .aop import BeanValidate, bean_validate_decorator
36
+
37
+ __version__ = "1.0.0"
38
+
39
+ __all__ = [
40
+ # 异常
41
+ "ConstraintViolation", "ValidationError",
42
+ # 约束
43
+ "Constraint",
44
+ "NotNull", "NotBlank", "NotEmpty",
45
+ "Size",
46
+ "Min", "Max",
47
+ "Positive", "PositiveOrZero", "Negative", "NegativeOrZero",
48
+ "Pattern", "Email",
49
+ "AssertTrue", "AssertFalse",
50
+ # 验证器
51
+ "BeanValidator",
52
+ # 方法级 AOP
53
+ "BeanValidate", "bean_validate_decorator",
54
+ "__version__",
55
+ ]
@@ -0,0 +1,141 @@
1
+ """SpringBootAI Bean Validation 方法级 AOP 注解 ``@BeanValidate``。
2
+
3
+ 把字段约束校验(``spring.validation.validator.BeanValidator``)接入既有 AOP 分发链路:
4
+ ``@BeanValidate`` 作为方法级 ``SpringAnnotation``,在 ``comprehensive_aop.ANNOTATION_DECORATORS``
5
+ 中注册装饰器,受管 Bean 方法被调用前自动校验指定参数对象。
6
+
7
+ 对齐 Jakarta Bean Validation 的方法级校验(``@Validated`` + 约束注解),但本模块只校验
8
+ **参数对象整体**(即对参数值调用 ``BeanValidator.validate_or_raise``),不校验单个标量参数。
9
+ 单个标量参数校验仍由既有 ``@Validate`` 切面承担(``comprehensive_aop.validate_decorator``)。
10
+
11
+ 用法::
12
+
13
+ from spring.validation import BeanValidate, NotBlank, BeanValidator
14
+ from spring.annotations import Service
15
+
16
+ class UserDto:
17
+ name = NotBlank()
18
+ def __init__(self, name=None): self.name = name
19
+
20
+ @Service
21
+ class UserService:
22
+ @BeanValidate("user") # 校验名为 user 的参数
23
+ def create(self, user: UserDto):
24
+ ...
25
+
26
+ @BeanValidate # 不传参:自动校验所有"类型含约束"的参数
27
+ def update(self, user: UserDto, flag: bool):
28
+ ...
29
+
30
+ 与 Java 的差异:Java 方法级校验需配合 ``MethodValidationPostProcessor`` 代理,且支持
31
+ ``@NotNull`` 直接标注在方法参数上;本模块不解析参数上的约束,仅对参数对象做整体校验。
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import functools
36
+ import inspect
37
+ from typing import Any, Callable, List, Optional, Union
38
+
39
+ from spring.annotations.core import SpringAnnotation
40
+
41
+ from .validator import BeanValidator
42
+ from .exceptions import ValidationError
43
+
44
+
45
+ class BeanValidate(SpringAnnotation):
46
+ """方法级校验注解:调用前自动用 ``BeanValidator`` 校验指定参数对象。
47
+
48
+ Args:
49
+ value: 指定要校验的参数名(str)或参数名列表(List[str])。
50
+ 不传时校验**所有**类型声明含字段约束的参数(自动探测)。
51
+ groups: 校验分组列表(透传给 ``BeanValidator.validate``)。
52
+ """
53
+
54
+ _annotation_type = "aop"
55
+
56
+ def __init__(
57
+ self,
58
+ value: Union[str, List[str], None] = None,
59
+ groups: Optional[List[type]] = None,
60
+ ):
61
+ if isinstance(value, str):
62
+ params: List[str] = [value]
63
+ elif value is None:
64
+ params = []
65
+ else:
66
+ params = list(value)
67
+ super().__init__(value=params, groups=groups or [])
68
+
69
+
70
+ def _param_has_constraints(cls: Any) -> bool:
71
+ """参数类型是否声明了字段约束(用于 ``@BeanValidate()`` 自动探测)。"""
72
+ if not isinstance(cls, type):
73
+ return False
74
+ try:
75
+ return bool(BeanValidator.get_constraints(cls))
76
+ except Exception:
77
+ return False
78
+
79
+
80
+ def bean_validate_decorator(annotation: BeanValidate):
81
+ """``@BeanValidate`` 的 AOP 装饰器工厂(注册到 ``comprehensive_aop.ANNOTATION_DECORATORS``)。
82
+
83
+ 在方法调用前,对指定参数对象执行 ``BeanValidator.validate_or_raise``;
84
+ 违反约束时抛出 ``ValidationError``,阻止方法执行。
85
+ """
86
+ target_params: List[str] = list(annotation.value)
87
+ groups = list(annotation.groups)
88
+
89
+ def decorator(func: Callable) -> Callable:
90
+ sig = inspect.signature(func)
91
+
92
+ def _resolve_targets(args, kwargs) -> List[str]:
93
+ """决定本次调用要校验的参数名列表。"""
94
+ if target_params:
95
+ # 显式指定的参数名
96
+ return [p for p in target_params]
97
+ # 自动探测:所有类型声明含约束的参数
98
+ targets: List[str] = []
99
+ for pname, param in sig.parameters.items():
100
+ if pname == "self":
101
+ continue
102
+ annotation_cls = param.annotation
103
+ if annotation_cls is inspect.Parameter.empty:
104
+ continue
105
+ if _param_has_constraints(annotation_cls):
106
+ targets.append(pname)
107
+ return targets
108
+
109
+ if inspect.iscoroutinefunction(func):
110
+ @functools.wraps(func)
111
+ async def async_wrapper(*args, **kwargs):
112
+ bound = sig.bind_partial(*args, **kwargs)
113
+ bound.apply_defaults()
114
+ for pname in _resolve_targets(args, kwargs):
115
+ if pname not in bound.arguments:
116
+ continue
117
+ val = bound.arguments[pname]
118
+ if val is None:
119
+ continue
120
+ BeanValidator.validate_or_raise(val, groups=groups)
121
+ return await func(*args, **kwargs)
122
+ return async_wrapper
123
+
124
+ @functools.wraps(func)
125
+ def wrapper(*args, **kwargs):
126
+ bound = sig.bind_partial(*args, **kwargs)
127
+ bound.apply_defaults()
128
+ for pname in _resolve_targets(args, kwargs):
129
+ if pname not in bound.arguments:
130
+ continue
131
+ val = bound.arguments[pname]
132
+ if val is None:
133
+ continue
134
+ BeanValidator.validate_or_raise(val, groups=groups)
135
+ return func(*args, **kwargs)
136
+ return wrapper
137
+
138
+ return decorator
139
+
140
+
141
+ __all__ = ["BeanValidate", "bean_validate_decorator", "ValidationError"]