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/cloud/feign.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Feign远程调用模块
|
|
3
|
+
提供声明式HTTP客户端功能
|
|
4
|
+
"""
|
|
5
|
+
import requests
|
|
6
|
+
import logging
|
|
7
|
+
import json
|
|
8
|
+
import inspect
|
|
9
|
+
from dataclasses import asdict, is_dataclass
|
|
10
|
+
from typing import Dict, Any, Optional, Type, Callable
|
|
11
|
+
from starlette.concurrency import run_in_threadpool
|
|
12
|
+
from spring.cloud.load_balancer import LoadBalancer
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("Spring.Cloud.Feign")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FeignClientProxy:
|
|
18
|
+
"""Feign客户端代理"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
service_name: str,
|
|
23
|
+
path: str = "",
|
|
24
|
+
url: str = "",
|
|
25
|
+
fallback: Type = None,
|
|
26
|
+
fallback_factory: Type = None,
|
|
27
|
+
timeout: float = 30,
|
|
28
|
+
pool_connections: int = 20,
|
|
29
|
+
pool_maxsize: int = 100,
|
|
30
|
+
):
|
|
31
|
+
self.service_name = service_name
|
|
32
|
+
self.path = path
|
|
33
|
+
self.url = url
|
|
34
|
+
self.fallback = fallback
|
|
35
|
+
self.fallback_factory = fallback_factory
|
|
36
|
+
self.timeout = timeout
|
|
37
|
+
self._load_balancer = LoadBalancer()
|
|
38
|
+
self._session = requests.Session()
|
|
39
|
+
adapter = requests.adapters.HTTPAdapter(
|
|
40
|
+
pool_connections=max(1, int(pool_connections)),
|
|
41
|
+
pool_maxsize=max(1, int(pool_maxsize)),
|
|
42
|
+
max_retries=0,
|
|
43
|
+
pool_block=True,
|
|
44
|
+
)
|
|
45
|
+
self._session.mount("http://", adapter)
|
|
46
|
+
self._session.mount("https://", adapter)
|
|
47
|
+
|
|
48
|
+
def close(self) -> None:
|
|
49
|
+
self._session.close()
|
|
50
|
+
|
|
51
|
+
def __enter__(self) -> "FeignClientProxy":
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
55
|
+
self.close()
|
|
56
|
+
|
|
57
|
+
def _get_base_url(self) -> str:
|
|
58
|
+
"""获取基础URL"""
|
|
59
|
+
if self.url:
|
|
60
|
+
return self.url
|
|
61
|
+
|
|
62
|
+
# 使用负载均衡获取服务实例
|
|
63
|
+
instances = self._load_balancer.get_instances(self.service_name)
|
|
64
|
+
if not instances:
|
|
65
|
+
raise Exception(f"No instances available for service: {self.service_name}")
|
|
66
|
+
|
|
67
|
+
instance = self._load_balancer.select_instance(instances)
|
|
68
|
+
return f"http://{instance['ip']}:{instance['port']}"
|
|
69
|
+
|
|
70
|
+
def _build_url(self, endpoint: str) -> str:
|
|
71
|
+
"""构建完整URL"""
|
|
72
|
+
base_url = self._get_base_url()
|
|
73
|
+
full_path = self.path.rstrip('/') + '/' + endpoint.lstrip('/')
|
|
74
|
+
return f"{base_url.rstrip('/')}/{full_path.lstrip('/')}"
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _jsonable(value: Any) -> Any:
|
|
78
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
79
|
+
return asdict(value)
|
|
80
|
+
if hasattr(value, 'model_dump') and callable(value.model_dump):
|
|
81
|
+
return value.model_dump()
|
|
82
|
+
if hasattr(value, 'dict') and callable(value.dict):
|
|
83
|
+
return value.dict()
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
def _call_fallback(self, fallback_method: Optional[str], error: Exception, args, kwargs):
|
|
87
|
+
if not self.fallback:
|
|
88
|
+
raise error
|
|
89
|
+
try:
|
|
90
|
+
if self.fallback_factory:
|
|
91
|
+
factory = self.fallback_factory()
|
|
92
|
+
fallback_instance = factory.create(error) if hasattr(factory, 'create') else factory(error)
|
|
93
|
+
else:
|
|
94
|
+
fallback_instance = self.fallback()
|
|
95
|
+
except TypeError:
|
|
96
|
+
fallback_instance = self.fallback()
|
|
97
|
+
method = getattr(fallback_instance, fallback_method or '', None)
|
|
98
|
+
if not callable(method):
|
|
99
|
+
raise error
|
|
100
|
+
return method(*args, **kwargs)
|
|
101
|
+
|
|
102
|
+
def request(
|
|
103
|
+
self,
|
|
104
|
+
method: str,
|
|
105
|
+
endpoint: str,
|
|
106
|
+
*,
|
|
107
|
+
params: Optional[Dict[str, Any]] = None,
|
|
108
|
+
json_data: Any = None,
|
|
109
|
+
data: Any = None,
|
|
110
|
+
headers: Optional[Dict[str, str]] = None,
|
|
111
|
+
timeout: Optional[float] = None,
|
|
112
|
+
fallback_method: Optional[str] = None,
|
|
113
|
+
call_args: tuple = (),
|
|
114
|
+
call_kwargs: Optional[dict] = None,
|
|
115
|
+
) -> Any:
|
|
116
|
+
"""Execute a declared Feign request and invoke its fallback if needed."""
|
|
117
|
+
url = self._build_url(endpoint)
|
|
118
|
+
call_kwargs = call_kwargs or {}
|
|
119
|
+
|
|
120
|
+
# 自动注入分布式事务XID头
|
|
121
|
+
req_headers = dict(headers) if headers else {}
|
|
122
|
+
try:
|
|
123
|
+
from spring.cloud.seata import seata_manager
|
|
124
|
+
xid = seata_manager.get_current_tx_id()
|
|
125
|
+
if xid:
|
|
126
|
+
seata_manager.inject_xid_headers(req_headers, xid)
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
# 自动注入追踪头(W3C traceparent)
|
|
131
|
+
try:
|
|
132
|
+
from spring.cloud.tracer import get_tracer
|
|
133
|
+
tracer = get_tracer()
|
|
134
|
+
if tracer.enabled:
|
|
135
|
+
tracer.inject_headers(req_headers)
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
response = self._session.request(
|
|
141
|
+
method.upper(),
|
|
142
|
+
url,
|
|
143
|
+
params=params,
|
|
144
|
+
json=self._jsonable(json_data) if json_data is not None else None,
|
|
145
|
+
data=data,
|
|
146
|
+
headers=req_headers,
|
|
147
|
+
timeout=self.timeout if timeout is None else timeout,
|
|
148
|
+
)
|
|
149
|
+
response.raise_for_status()
|
|
150
|
+
if not response.content:
|
|
151
|
+
return None
|
|
152
|
+
try:
|
|
153
|
+
return response.json()
|
|
154
|
+
except (ValueError, json.JSONDecodeError):
|
|
155
|
+
return response.text
|
|
156
|
+
except Exception as error:
|
|
157
|
+
logger.error("Feign %s request failed: %s, error: %s", method, url, error)
|
|
158
|
+
return self._call_fallback(fallback_method, error, call_args, call_kwargs)
|
|
159
|
+
|
|
160
|
+
async def arequest(self, method: str, endpoint: str, **kwargs) -> Any:
|
|
161
|
+
"""Execute the synchronous requests client without blocking the ASGI loop."""
|
|
162
|
+
return await run_in_threadpool(self.request, method, endpoint, **kwargs)
|
|
163
|
+
|
|
164
|
+
def get(self, endpoint: str, params: Dict[str, Any] = None, headers: Dict[str, str] = None) -> Any:
|
|
165
|
+
"""
|
|
166
|
+
发送GET请求
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
endpoint: 端点路径
|
|
170
|
+
params: 查询参数
|
|
171
|
+
headers: 请求头
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
响应数据
|
|
175
|
+
"""
|
|
176
|
+
url = self._build_url(endpoint)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
response = self._session.get(
|
|
180
|
+
url, params=params, headers=headers, timeout=self.timeout
|
|
181
|
+
)
|
|
182
|
+
response.raise_for_status()
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
return response.json()
|
|
186
|
+
except json.JSONDecodeError:
|
|
187
|
+
return response.text
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"Feign GET request failed: {url}, error: {e}")
|
|
190
|
+
|
|
191
|
+
# 尝试降级处理
|
|
192
|
+
if self.fallback:
|
|
193
|
+
fallback_instance = self.fallback()
|
|
194
|
+
method = getattr(fallback_instance, endpoint.replace('/', '_'), None)
|
|
195
|
+
if method and callable(method):
|
|
196
|
+
return method(params=params, headers=headers)
|
|
197
|
+
|
|
198
|
+
raise
|
|
199
|
+
|
|
200
|
+
def post(self, endpoint: str, data: Dict[str, Any] = None, json_data: Dict[str, Any] = None,
|
|
201
|
+
headers: Dict[str, str] = None) -> Any:
|
|
202
|
+
"""
|
|
203
|
+
发送POST请求
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
endpoint: 端点路径
|
|
207
|
+
data: 表单数据
|
|
208
|
+
json_data: JSON数据
|
|
209
|
+
headers: 请求头
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
响应数据
|
|
213
|
+
"""
|
|
214
|
+
url = self._build_url(endpoint)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
response = self._session.post(
|
|
218
|
+
url, data=data, json=json_data, headers=headers, timeout=self.timeout
|
|
219
|
+
)
|
|
220
|
+
response.raise_for_status()
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
return response.json()
|
|
224
|
+
except json.JSONDecodeError:
|
|
225
|
+
return response.text
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Feign POST request failed: {url}, error: {e}")
|
|
228
|
+
|
|
229
|
+
if self.fallback:
|
|
230
|
+
fallback_instance = self.fallback()
|
|
231
|
+
method = getattr(fallback_instance, endpoint.replace('/', '_'), None)
|
|
232
|
+
if method and callable(method):
|
|
233
|
+
return method(data=data, json_data=json_data, headers=headers)
|
|
234
|
+
|
|
235
|
+
raise
|
|
236
|
+
|
|
237
|
+
def put(self, endpoint: str, data: Dict[str, Any] = None, json_data: Dict[str, Any] = None,
|
|
238
|
+
headers: Dict[str, str] = None) -> Any:
|
|
239
|
+
"""
|
|
240
|
+
发送PUT请求
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
endpoint: 端点路径
|
|
244
|
+
data: 表单数据
|
|
245
|
+
json_data: JSON数据
|
|
246
|
+
headers: 请求头
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
响应数据
|
|
250
|
+
"""
|
|
251
|
+
url = self._build_url(endpoint)
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
response = self._session.put(
|
|
255
|
+
url, data=data, json=json_data, headers=headers, timeout=self.timeout
|
|
256
|
+
)
|
|
257
|
+
response.raise_for_status()
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
return response.json()
|
|
261
|
+
except json.JSONDecodeError:
|
|
262
|
+
return response.text
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.error(f"Feign PUT request failed: {url}, error: {e}")
|
|
265
|
+
|
|
266
|
+
if self.fallback:
|
|
267
|
+
fallback_instance = self.fallback()
|
|
268
|
+
method = getattr(fallback_instance, endpoint.replace('/', '_'), None)
|
|
269
|
+
if method and callable(method):
|
|
270
|
+
return method(data=data, json_data=json_data, headers=headers)
|
|
271
|
+
|
|
272
|
+
raise
|
|
273
|
+
|
|
274
|
+
def delete(self, endpoint: str, params: Dict[str, Any] = None, headers: Dict[str, str] = None) -> Any:
|
|
275
|
+
"""
|
|
276
|
+
发送DELETE请求
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
endpoint: 端点路径
|
|
280
|
+
params: 查询参数
|
|
281
|
+
headers: 请求头
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
响应数据
|
|
285
|
+
"""
|
|
286
|
+
url = self._build_url(endpoint)
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
response = self._session.delete(
|
|
290
|
+
url, params=params, headers=headers, timeout=self.timeout
|
|
291
|
+
)
|
|
292
|
+
response.raise_for_status()
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
return response.json()
|
|
296
|
+
except json.JSONDecodeError:
|
|
297
|
+
return response.text
|
|
298
|
+
except Exception as e:
|
|
299
|
+
logger.error(f"Feign DELETE request failed: {url}, error: {e}")
|
|
300
|
+
|
|
301
|
+
if self.fallback:
|
|
302
|
+
fallback_instance = self.fallback()
|
|
303
|
+
method = getattr(fallback_instance, endpoint.replace('/', '_'), None)
|
|
304
|
+
if method and callable(method):
|
|
305
|
+
return method(params=params, headers=headers)
|
|
306
|
+
|
|
307
|
+
raise
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class FeignClientFactory:
|
|
311
|
+
"""Feign客户端工厂"""
|
|
312
|
+
|
|
313
|
+
_clients: Dict[str, FeignClientProxy] = {}
|
|
314
|
+
|
|
315
|
+
@classmethod
|
|
316
|
+
def get_client(cls, service_name: str) -> FeignClientProxy:
|
|
317
|
+
"""
|
|
318
|
+
获取Feign客户端
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
service_name: 服务名称
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Feign客户端代理
|
|
325
|
+
"""
|
|
326
|
+
if service_name not in cls._clients:
|
|
327
|
+
cls._clients[service_name] = FeignClientProxy(service_name)
|
|
328
|
+
|
|
329
|
+
return cls._clients[service_name]
|
|
330
|
+
|
|
331
|
+
@classmethod
|
|
332
|
+
def register_client(cls, service_name: str, client: FeignClientProxy):
|
|
333
|
+
"""
|
|
334
|
+
注册Feign客户端
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
service_name: 服务名称
|
|
338
|
+
client: Feign客户端代理
|
|
339
|
+
"""
|
|
340
|
+
cls._clients[service_name] = client
|
|
341
|
+
|
|
342
|
+
@classmethod
|
|
343
|
+
def close_all(cls) -> None:
|
|
344
|
+
for client in cls._clients.values():
|
|
345
|
+
client.close()
|
|
346
|
+
cls._clients.clear()
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def create_feign_client(service_name: str, path: str = "", url: str = "",
|
|
350
|
+
fallback: Type = None,
|
|
351
|
+
fallback_factory: Type = None,
|
|
352
|
+
timeout: float = 30) -> FeignClientProxy:
|
|
353
|
+
"""
|
|
354
|
+
创建Feign客户端
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
service_name: 服务名称
|
|
358
|
+
path: 路径前缀
|
|
359
|
+
url: 直接URL(调试用)
|
|
360
|
+
fallback: 降级实现类
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Feign客户端代理
|
|
364
|
+
"""
|
|
365
|
+
return FeignClientProxy(service_name, path, url, fallback, fallback_factory, timeout)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def create_declared_feign_client(client_class: Type, annotation: Any) -> Any:
|
|
369
|
+
"""Create a typed proxy from a ``@FeignClient`` class declaration.
|
|
370
|
+
|
|
371
|
+
Method mappings use the same SpringBootAI ``@RequestMapping`` family as web
|
|
372
|
+
controllers. The generated object subclasses the declaration class, so
|
|
373
|
+
normal type-based IoC injection and ``isinstance`` checks continue to work.
|
|
374
|
+
"""
|
|
375
|
+
from spring.annotations.core import (
|
|
376
|
+
RequestMapping, GetMapping, PostMapping, PutMapping,
|
|
377
|
+
PatchMapping, DeleteMapping, RequestParam, PathVariable,
|
|
378
|
+
RequestBody, RequestHeader,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
proxy = FeignClientProxy(
|
|
382
|
+
annotation.value,
|
|
383
|
+
path=annotation.path,
|
|
384
|
+
url=annotation.url,
|
|
385
|
+
fallback=annotation.fallback,
|
|
386
|
+
fallback_factory=annotation.fallback_factory,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
mappings = (RequestMapping, GetMapping, PostMapping, PutMapping, PatchMapping, DeleteMapping)
|
|
390
|
+
generated = {}
|
|
391
|
+
for method_name, method in inspect.getmembers(client_class, inspect.isfunction):
|
|
392
|
+
mapping = next((item for item in getattr(method, '__spring_annotations__', []) if isinstance(item, mappings)), None)
|
|
393
|
+
if mapping is None:
|
|
394
|
+
continue
|
|
395
|
+
raw_path = mapping.path
|
|
396
|
+
endpoint_path = raw_path[0] if isinstance(raw_path, list) else raw_path
|
|
397
|
+
endpoint_path = endpoint_path or method_name
|
|
398
|
+
http_method = (mapping.method or ['GET'])[0].upper()
|
|
399
|
+
signature = inspect.signature(method)
|
|
400
|
+
parameters = [p for p in signature.parameters.values() if p.name != 'self']
|
|
401
|
+
|
|
402
|
+
def make_call(name, endpoint, verb, original, params):
|
|
403
|
+
def call(self, *args, **kwargs):
|
|
404
|
+
bound = inspect.signature(original).bind_partial(None, *args, **kwargs)
|
|
405
|
+
bound.apply_defaults()
|
|
406
|
+
values = dict(bound.arguments)
|
|
407
|
+
values.pop('self', None)
|
|
408
|
+
path_values = {}
|
|
409
|
+
query = {}
|
|
410
|
+
body = None
|
|
411
|
+
headers = {}
|
|
412
|
+
for parameter in params:
|
|
413
|
+
value = values.get(parameter.name)
|
|
414
|
+
marker = parameter.default
|
|
415
|
+
if isinstance(marker, PathVariable) or '{' + parameter.name + '}' in endpoint:
|
|
416
|
+
path_values[marker.name if isinstance(marker, PathVariable) and marker.name else parameter.name] = value
|
|
417
|
+
elif isinstance(marker, RequestHeader):
|
|
418
|
+
headers[marker.name or parameter.name.replace('_', '-')] = value
|
|
419
|
+
elif isinstance(marker, RequestBody):
|
|
420
|
+
body = value
|
|
421
|
+
elif isinstance(marker, RequestParam):
|
|
422
|
+
query[marker.name or parameter.name] = value
|
|
423
|
+
elif verb in {'GET', 'DELETE'}:
|
|
424
|
+
query[parameter.name] = value
|
|
425
|
+
elif body is None:
|
|
426
|
+
body = value
|
|
427
|
+
else:
|
|
428
|
+
query[parameter.name] = value
|
|
429
|
+
try:
|
|
430
|
+
endpoint_rendered = endpoint.format(**path_values)
|
|
431
|
+
except KeyError as exc:
|
|
432
|
+
raise ValueError(f"Feign 路径缺少参数: {exc.args[0]}") from exc
|
|
433
|
+
return proxy.request(
|
|
434
|
+
verb,
|
|
435
|
+
endpoint_rendered,
|
|
436
|
+
params=query or None,
|
|
437
|
+
json_data=body if verb not in {'GET', 'DELETE'} else None,
|
|
438
|
+
headers=headers or None,
|
|
439
|
+
timeout=proxy.timeout,
|
|
440
|
+
fallback_method=name,
|
|
441
|
+
call_args=args,
|
|
442
|
+
call_kwargs=kwargs,
|
|
443
|
+
)
|
|
444
|
+
call.__name__ = name
|
|
445
|
+
call.__doc__ = getattr(original, '__doc__', None)
|
|
446
|
+
if inspect.iscoroutinefunction(original):
|
|
447
|
+
async def async_call(self, *args, **kwargs):
|
|
448
|
+
return await run_in_threadpool(call, self, *args, **kwargs)
|
|
449
|
+
async_call.__name__ = name
|
|
450
|
+
async_call.__doc__ = call.__doc__
|
|
451
|
+
return async_call
|
|
452
|
+
return call
|
|
453
|
+
|
|
454
|
+
generated[method_name] = make_call(method_name, endpoint_path, http_method, method, parameters)
|
|
455
|
+
|
|
456
|
+
original_destroy = getattr(client_class, 'destroy', None)
|
|
457
|
+
|
|
458
|
+
def destroy(self):
|
|
459
|
+
try:
|
|
460
|
+
if callable(original_destroy):
|
|
461
|
+
original_destroy(self)
|
|
462
|
+
finally:
|
|
463
|
+
proxy.close()
|
|
464
|
+
|
|
465
|
+
generated['destroy'] = destroy
|
|
466
|
+
implementation = type(f"{client_class.__name__}FeignProxy", (client_class,), generated)
|
|
467
|
+
instance = implementation()
|
|
468
|
+
instance.__feign_proxy__ = proxy
|
|
469
|
+
return instance
|