springbootAI 1.8.4__tar.gz → 1.8.6__tar.gz
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.
- {springbootai-1.8.4/springbootAI.egg-info → springbootai-1.8.6}/PKG-INFO +1 -1
- {springbootai-1.8.4 → springbootai-1.8.6}/pyproject.toml +1 -1
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/__init__.py +1 -1
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/discovery.py +48 -18
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/config/config_loader.py +284 -57
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/context/application_context.py +8 -0
- springbootai-1.8.6/spring/logging/loguru_logger.py +419 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/main.py +145 -69
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/messaging/rabbitmq.py +50 -14
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/database.py +34 -6
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/utils/banner.py +1 -1
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/utils/redis_client.py +31 -9
- {springbootai-1.8.4 → springbootai-1.8.6/springbootAI.egg-info}/PKG-INFO +1 -1
- {springbootai-1.8.4 → springbootai-1.8.6}/springbootAI.egg-info/SOURCES.txt +3 -0
- springbootai-1.8.6/tests/test_component_init_error.py +171 -0
- springbootai-1.8.6/tests/test_config_fixes_185.py +547 -0
- springbootai-1.8.6/tests/test_logging_config.py +152 -0
- springbootai-1.8.4/spring/logging/loguru_logger.py +0 -233
- {springbootai-1.8.4 → springbootai-1.8.6}/LICENSE +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/MANIFEST.in +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/README.md +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/requirements.txt +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/setup.cfg +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/setup.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/advisors.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/annotations.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/autoconfig.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/core.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/etl.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/memory.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/observability.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/providers.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/resilience.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/tools.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/vectorstore.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/annotations/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/annotations/cache.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/annotations/cloud.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/annotations/conditional.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/annotations/core.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/annotations/messaging.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/aop/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/aop/cloud_aop.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/aop/comprehensive_aop.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/aop/method_interceptor.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/aop/proxy_factory.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/feign.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/gateway.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/load_balancer.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/seata.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/sentinel.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/tracer.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/transaction_store.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/config/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/config/binding.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/context/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/context/bean_definition.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/context/bean_factory.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/context/registry.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/context/scanner.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/core/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/core/graceful_shutdown.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/core/typing_utils.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/annotations.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/converters.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/easy_csv.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/exceptions.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/reader.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/writer.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/data/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/data/page.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/data/repository.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/data/specification.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/datasource/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/datasource/annotations.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/datasource/context.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/datasource/dynamic.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/event/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/event/publisher.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/annotations.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/converters.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/easy_excel.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/exceptions.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/reader.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/style.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/writer.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/accessor.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/auto_config.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/holder.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/locale.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/locale_resolver.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/message_source.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/middleware.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/properties.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/sources.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/logging/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/messaging/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/monitoring/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/monitoring/prometheus.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/ddl_auto.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/migration.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/mybatis_integration.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/annotations/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/annotations/annotations.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/cache/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/cache/cache.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/cache/redis_cache.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/circuit_breaker/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/configuration.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/core/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/core/sql_session.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/core/sql_session_factory.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/dialect/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/dialect/dialect.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/dynamic_sql/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/interceptor/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/interceptor/interceptor.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/mapper/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/mapper/mapper.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/metrics/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/metrics/metrics.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/pool/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/pool/connection_pool.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/security/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/security/access_control.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/security/password_encoder.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/security/sensitive_data_masker.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/security/sql_injection_detector.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/transaction/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/transaction/transaction.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/type_handler/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/type_handler/type_handler.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/version.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/xml_parser/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/xml_parser/xml_parser.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/retry/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/retry/retry_annotations.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/retry/retry_decorator.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/scheduling/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/scheduling/scheduler.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/security/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/security/jwt_utils.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/security/replay_protection.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/security/secret_manager.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/security/security_aop.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/security/security_context.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/test/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/test/slicing.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/tracing/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/tracing/skywalking.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/tx/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/tx/events.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/tx/synchronization.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/utils/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/utils/bean_utils.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/utils/logger.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/validation/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/validation/aop.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/validation/constraints.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/validation/exceptions.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/validation/validator.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/actuator.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/exception_handler.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/health.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/interceptor.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/result.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/swagger.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/web_context.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/__init__.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/annotations.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/broker.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/exceptions.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/handler.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/router.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/session.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/springbootAI.egg-info/dependency_links.txt +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/springbootAI.egg-info/entry_points.txt +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/springbootAI.egg-info/requires.txt +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/springbootAI.egg-info/top_level.txt +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/sql/seata_undo_log.sql +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_actuator.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_ai_module.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_annotation_combinations.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_annotations_contract.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_aop_annotations_full.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_bean_utils.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_cache_annotations.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_cloud_embedded_full.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_conditional_annotations.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_config_binding.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_connection_resilience.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_core_annotations_full.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_csv_module.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_data_repository.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_datasource_routing.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_ddl_auto.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_di_config_event_full.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_excel_module.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_i18n_module.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_jpa_version_transient.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_new_features.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_orm_pymybatis_full.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_production_readiness.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_pymybatis_contract.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_seata_durable_store.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_security.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_security_full.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_swagger_module.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_test_slicing.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_transactional_events.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_validation_module.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_web_annotations_full.py +0 -0
- {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_websocket_module.py +0 -0
|
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
|
|
|
12
12
|
[project]
|
|
13
13
|
# 发行名 springbootAI(PyPI 包名,pip install springbootAI);导入包名仍为 spring
|
|
14
14
|
name = "springbootAI"
|
|
15
|
-
version = "1.8.
|
|
15
|
+
version = "1.8.6"
|
|
16
16
|
description = "Python版Spring Boot框架,集成PyMyBatis ORM"
|
|
17
17
|
readme = "README.md"
|
|
18
18
|
license = "MIT"
|
|
@@ -32,20 +32,9 @@ class NacosDiscoveryClient:
|
|
|
32
32
|
|
|
33
33
|
def __init__(self, server_addr: str = "localhost:8848", namespace: str = "", group: str = "DEFAULT_GROUP", username: str = "", password: str = ""):
|
|
34
34
|
if hasattr(self, '_initialized'):
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
or self.group != group
|
|
39
|
-
or self.username != username
|
|
40
|
-
or self.password != password
|
|
41
|
-
):
|
|
42
|
-
self.server_addr = server_addr
|
|
43
|
-
self.namespace = namespace
|
|
44
|
-
self.group = group
|
|
45
|
-
self.username = username
|
|
46
|
-
self.password = password
|
|
47
|
-
self._client = None
|
|
48
|
-
self._ready = False
|
|
35
|
+
# 已初始化:委托给 configure() 做原地更新,保持单一更新路径
|
|
36
|
+
self.configure(server_addr=server_addr, namespace=namespace,
|
|
37
|
+
group=group, username=username, password=password)
|
|
49
38
|
return
|
|
50
39
|
self.server_addr = server_addr
|
|
51
40
|
self.namespace = namespace
|
|
@@ -58,6 +47,44 @@ class NacosDiscoveryClient:
|
|
|
58
47
|
self._ip: str = "127.0.0.1"
|
|
59
48
|
self._port: int = 8080
|
|
60
49
|
self._initialized = True
|
|
50
|
+
|
|
51
|
+
def configure(self, server_addr: Optional[str] = None, namespace: Optional[str] = None,
|
|
52
|
+
group: Optional[str] = None, username: Optional[str] = None,
|
|
53
|
+
password: Optional[str] = None) -> None:
|
|
54
|
+
"""重新配置单例的 Nacos 连接参数(读取配置后调用)。
|
|
55
|
+
|
|
56
|
+
``NacosDiscoveryClient`` 为单例,``__init__`` 的 ``_initialized`` 守卫会
|
|
57
|
+
阻止后续 ``__init__`` 更新参数。``init_discovery`` 读取 ``application.yml``
|
|
58
|
+
的 ``discovery.*`` 后,必须通过本方法重新配置,否则连接参数停留在默认值。
|
|
59
|
+
|
|
60
|
+
参数变化时重置已建立的客户端连接,强制下次 ``connect()`` 重建。
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
server_addr: Nacos 服务地址,None 表示保留原值
|
|
64
|
+
namespace: 命名空间,None 表示保留原值
|
|
65
|
+
group: 分组,None 表示保留原值
|
|
66
|
+
username: 用户名,None 表示保留原值
|
|
67
|
+
password: 密码,None 表示保留原值
|
|
68
|
+
"""
|
|
69
|
+
changed = False
|
|
70
|
+
if server_addr is not None and self.server_addr != server_addr:
|
|
71
|
+
self.server_addr = server_addr
|
|
72
|
+
changed = True
|
|
73
|
+
if namespace is not None and self.namespace != namespace:
|
|
74
|
+
self.namespace = namespace
|
|
75
|
+
changed = True
|
|
76
|
+
if group is not None and self.group != group:
|
|
77
|
+
self.group = group
|
|
78
|
+
changed = True
|
|
79
|
+
if username is not None and self.username != username:
|
|
80
|
+
self.username = username
|
|
81
|
+
changed = True
|
|
82
|
+
if password is not None and self.password != password:
|
|
83
|
+
self.password = password
|
|
84
|
+
changed = True
|
|
85
|
+
if changed:
|
|
86
|
+
self._client = None
|
|
87
|
+
self._ready = False
|
|
61
88
|
|
|
62
89
|
def connect(self) -> None:
|
|
63
90
|
"""连接Nacos"""
|
|
@@ -347,13 +374,16 @@ nacos_client = NacosDiscoveryClient()
|
|
|
347
374
|
def init_discovery(config: dict) -> None:
|
|
348
375
|
"""
|
|
349
376
|
初始化服务注册发现
|
|
350
|
-
|
|
377
|
+
|
|
378
|
+
通过 ``configure`` 重新配置单例 ``NacosDiscoveryClient``,使 ``discovery.*``
|
|
379
|
+
配置生效。直接 ``NacosDiscoveryClient(...)`` 因单例 ``_initialized`` 守卫不会
|
|
380
|
+
更新参数。
|
|
381
|
+
|
|
351
382
|
Args:
|
|
352
383
|
config: 配置字典,包含server_addr, namespace, group等
|
|
353
384
|
"""
|
|
354
|
-
#
|
|
355
|
-
|
|
356
|
-
nacos_client.__init__(
|
|
385
|
+
# 单例原地更新配置,避免 _initialized 守卫导致配置被忽略
|
|
386
|
+
nacos_client.configure(
|
|
357
387
|
server_addr=config.get('server_addr', 'localhost:8848'),
|
|
358
388
|
namespace=config.get('namespace', ''),
|
|
359
389
|
group=config.get('group', 'DEFAULT_GROUP'),
|
|
@@ -17,15 +17,46 @@ class ConfigurationError(ValueError):
|
|
|
17
17
|
"""应用配置缺失、格式错误或不满足生产安全要求。"""
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
# 匹配非字母数字字符(kebab/camel/snake 分隔符)用于松散匹配规范化
|
|
21
|
+
_NON_ALNUM = re.compile(r'[^0-9a-zA-Z]+')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _normalize_name(name: str) -> str:
|
|
25
|
+
"""规范化命名:去除分隔符并小写,用于 ``get()`` 的松散匹配。
|
|
26
|
+
|
|
27
|
+
``log_dir`` / ``log-dir`` / ``LOG_DIR`` / ``log.dir`` → ``logdir``。
|
|
28
|
+
与 ``spring.config.binding._normalize`` 保持一致,避免循环导入在此独立定义。
|
|
29
|
+
"""
|
|
30
|
+
if not isinstance(name, str):
|
|
31
|
+
return str(name).lower()
|
|
32
|
+
return _NON_ALNUM.sub('', name).lower()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _to_bool(value: Any, default: bool = False) -> bool:
|
|
36
|
+
"""统一布尔转换:接受 ``true/1/yes/on``(大小写不敏感),其余为 False。
|
|
37
|
+
|
|
38
|
+
与 ``spring.config.binding._coerce`` 的布尔规则保持一致,避免
|
|
39
|
+
``REDIS_ENABLED=1`` 在 ``@ConfigurationProperties`` 绑定为 True 而在
|
|
40
|
+
``_override_with_env`` 中被识别为 False 的行为不一致。
|
|
41
|
+
"""
|
|
42
|
+
if isinstance(value, bool):
|
|
43
|
+
return value
|
|
44
|
+
if value is None:
|
|
45
|
+
return default
|
|
46
|
+
return str(value).strip().lower() in ('true', '1', 'yes', 'on')
|
|
47
|
+
|
|
48
|
+
|
|
20
49
|
class ConfigLoader:
|
|
21
50
|
"""配置加载器"""
|
|
22
51
|
|
|
23
52
|
# ApplicationContext updates this after startup so later ``ConfigLoader()``
|
|
24
53
|
# calls resolve the same application.yml instead of depending on CWD.
|
|
25
54
|
_default_base_path: Optional[str] = None
|
|
26
|
-
|
|
27
|
-
# 环境变量替换模式:${ENV_VAR} 或 ${ENV_VAR:default}
|
|
55
|
+
|
|
56
|
+
# 环境变量替换模式:${ENV_VAR} 或 ${ENV_VAR:default}(单层,用于 fullmatch 判定)
|
|
28
57
|
_ENV_VAR_PATTERN = re.compile(r'\$\{([^}]+)\}')
|
|
58
|
+
# 最内层占位符:内容不含 ${,用于迭代解析嵌套占位符 ${A:${B:default}}
|
|
59
|
+
_INNER_ENV_VAR_PATTERN = re.compile(r'\$\{([^${}]*)\}')
|
|
29
60
|
_VALUE_EXPRESSION_PATTERN = re.compile(r'^\$\{([^}:]+)(?::(.*))?\}$')
|
|
30
61
|
_MISSING = object()
|
|
31
62
|
|
|
@@ -42,47 +73,80 @@ class ConfigLoader:
|
|
|
42
73
|
self._config: Dict[str, Any] = {}
|
|
43
74
|
self._load_config()
|
|
44
75
|
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _is_single_placeholder(value: str) -> bool:
|
|
78
|
+
"""判断字符串是否为单个完整的占位符(支持嵌套 ``${A:${B:default}}``)。
|
|
79
|
+
|
|
80
|
+
用于决定解析后是否做类型推断(``yaml.safe_load``)。
|
|
81
|
+
用括号深度计数识别单个平衡占位符,避免正则 ``[^}]+`` 无法处理嵌套 ``}``。
|
|
82
|
+
"""
|
|
83
|
+
value = value.strip()
|
|
84
|
+
if not (value.startswith('${') and value.endswith('}')):
|
|
85
|
+
return False
|
|
86
|
+
depth = 0
|
|
87
|
+
for i, c in enumerate(value):
|
|
88
|
+
if c == '{':
|
|
89
|
+
depth += 1
|
|
90
|
+
elif c == '}':
|
|
91
|
+
depth -= 1
|
|
92
|
+
if depth == 0 and i != len(value) - 1:
|
|
93
|
+
return False # 在结束前就已闭合 → 非单个占位符
|
|
94
|
+
return depth == 0
|
|
95
|
+
|
|
45
96
|
def _resolve_env_var(self, value: str) -> Any:
|
|
46
97
|
"""
|
|
47
98
|
解析环境变量引用
|
|
48
|
-
|
|
99
|
+
|
|
49
100
|
支持格式:
|
|
50
101
|
- ${ENV_VAR} - 直接读取环境变量
|
|
51
102
|
- ${ENV_VAR:default} - 读取环境变量,不存在使用默认值
|
|
52
|
-
|
|
103
|
+
- ${A:${B:default}} - 嵌套占位符(迭代解析最内层)
|
|
104
|
+
|
|
53
105
|
Args:
|
|
54
106
|
value: 包含环境变量引用的字符串
|
|
55
|
-
|
|
107
|
+
|
|
56
108
|
Returns:
|
|
57
|
-
|
|
109
|
+
解析后的值(单个占位符时做类型推断)
|
|
58
110
|
"""
|
|
59
111
|
if not isinstance(value, str):
|
|
60
112
|
return value
|
|
61
|
-
|
|
62
|
-
|
|
113
|
+
|
|
114
|
+
# 判断整个值是否为单个占位符(支持嵌套),用于类型推断
|
|
115
|
+
exact_placeholder = self._is_single_placeholder(value)
|
|
63
116
|
|
|
64
117
|
def replace_env(match):
|
|
65
118
|
env_spec = match.group(1)
|
|
66
|
-
|
|
119
|
+
|
|
67
120
|
# 检查是否有默认值
|
|
68
121
|
if ':' in env_spec:
|
|
69
122
|
env_name, default_value = env_spec.split(':', 1)
|
|
70
123
|
else:
|
|
71
124
|
env_name = env_spec
|
|
72
125
|
default_value = None
|
|
73
|
-
|
|
126
|
+
|
|
74
127
|
# 从环境变量获取值
|
|
75
128
|
env_value = os.environ.get(env_name.strip())
|
|
76
|
-
|
|
129
|
+
|
|
77
130
|
# 如果环境变量不存在,使用默认值
|
|
78
131
|
if env_value is None:
|
|
79
132
|
if default_value is None:
|
|
80
133
|
raise ConfigurationError(f"必需的环境变量 {env_name.strip()} 未设置")
|
|
81
134
|
return default_value
|
|
82
|
-
|
|
135
|
+
|
|
83
136
|
return env_value
|
|
84
|
-
|
|
85
|
-
|
|
137
|
+
|
|
138
|
+
# 迭代解析最内层占位符,支持嵌套 ${A:${B:default}}
|
|
139
|
+
# 每轮用 _INNER_ENV_VAR_PATTERN 替换所有不含 ${ 的最内层占位符,
|
|
140
|
+
# 直到没有变化或达到最大迭代次数(防止无限循环)
|
|
141
|
+
resolved = value
|
|
142
|
+
prev = None
|
|
143
|
+
iterations = 0
|
|
144
|
+
while prev != resolved and iterations < 10:
|
|
145
|
+
prev = resolved
|
|
146
|
+
resolved = self._INNER_ENV_VAR_PATTERN.sub(replace_env, resolved)
|
|
147
|
+
iterations += 1
|
|
148
|
+
|
|
149
|
+
# 整个值是单个占位符时,做类型推断(如 "8080" → 8080, "true" → True)
|
|
86
150
|
if exact_placeholder:
|
|
87
151
|
parsed = yaml.safe_load(resolved)
|
|
88
152
|
if not isinstance(parsed, (dict, list)):
|
|
@@ -108,9 +172,37 @@ class ConfigLoader:
|
|
|
108
172
|
else:
|
|
109
173
|
return config
|
|
110
174
|
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
|
177
|
+
"""深度合并两个配置字典:``override`` 覆盖 ``base``,子字典递归合并。
|
|
178
|
+
|
|
179
|
+
对齐 Spring Boot 的 profile 合并语义:profile 特定配置覆盖主配置,
|
|
180
|
+
但主配置中未涉及的键保留。返回新字典,不修改入参。
|
|
181
|
+
"""
|
|
182
|
+
if not isinstance(base, dict):
|
|
183
|
+
base = {}
|
|
184
|
+
if not isinstance(override, dict):
|
|
185
|
+
return deepcopy(base)
|
|
186
|
+
result = deepcopy(base)
|
|
187
|
+
for key, value in override.items():
|
|
188
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
189
|
+
result[key] = ConfigLoader._deep_merge(result[key], value)
|
|
190
|
+
else:
|
|
191
|
+
result[key] = deepcopy(value)
|
|
192
|
+
return result
|
|
193
|
+
|
|
194
|
+
def _resolve_profile_path(self, profile: str) -> Optional[str]:
|
|
195
|
+
"""解析 ``application-{profile}.yml`` 路径,与主配置同目录。"""
|
|
196
|
+
if not profile or profile == 'default':
|
|
197
|
+
return None
|
|
198
|
+
config_dir = os.path.dirname(os.path.abspath(self.config_path))
|
|
199
|
+
profile_file = f"application-{profile}.yml"
|
|
200
|
+
candidate = os.path.join(config_dir, profile_file)
|
|
201
|
+
return candidate if os.path.exists(candidate) else None
|
|
202
|
+
|
|
111
203
|
def _load_config(self):
|
|
112
204
|
"""加载配置"""
|
|
113
|
-
# 1. 尝试从YAML
|
|
205
|
+
# 1. 尝试从YAML文件加载主配置
|
|
114
206
|
if os.path.exists(self.config_path):
|
|
115
207
|
try:
|
|
116
208
|
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
@@ -121,14 +213,61 @@ class ConfigLoader:
|
|
|
121
213
|
except Exception as e:
|
|
122
214
|
logger.error(f"Failed to load config from {self.config_path}: {e}")
|
|
123
215
|
raise ConfigurationError(f"无法加载配置文件 {self.config_path}") from e
|
|
124
|
-
|
|
216
|
+
|
|
217
|
+
# 1.5 加载 profile 特定配置文件并深度合并(application-{profile}.yml 覆盖主配置)
|
|
218
|
+
# profile 来自主配置的 spring.profiles.active 或 SPRING_PROFILES_ACTIVE 环境变量。
|
|
219
|
+
# 在占位符解析之前合并,使 profile 文件中的占位符也能被解析。
|
|
220
|
+
profile = (
|
|
221
|
+
os.getenv('SPRING_PROFILES_ACTIVE')
|
|
222
|
+
or self._config.get('spring', {}).get('profiles', {}).get('active', 'default')
|
|
223
|
+
)
|
|
224
|
+
if profile:
|
|
225
|
+
profile = str(profile).strip()
|
|
226
|
+
profile_path = self._resolve_profile_path(profile) if profile else None
|
|
227
|
+
if profile_path:
|
|
228
|
+
try:
|
|
229
|
+
with open(profile_path, 'r', encoding='utf-8') as f:
|
|
230
|
+
profile_config = yaml.safe_load(f) or {}
|
|
231
|
+
if isinstance(profile_config, dict):
|
|
232
|
+
self._config = self._deep_merge(self._config, profile_config)
|
|
233
|
+
logger.info(f"Loaded profile config from {profile_path} (profile={profile})")
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.warning(f"Failed to load profile config {profile_path}: {e}")
|
|
236
|
+
|
|
125
237
|
# 2. 解析配置中的环境变量占位符
|
|
126
238
|
self._config = self._resolve_config_recursive(self._config)
|
|
127
|
-
|
|
239
|
+
|
|
128
240
|
# 3. 从环境变量覆盖配置
|
|
129
241
|
self._override_with_env()
|
|
242
|
+
# 4. 从命令行参数覆盖配置(优先级最高)
|
|
243
|
+
self._override_with_cli_args()
|
|
130
244
|
self._validate_config()
|
|
131
245
|
|
|
246
|
+
@staticmethod
|
|
247
|
+
def _get_env_any(*names: str, default: Any = None) -> Any:
|
|
248
|
+
"""返回第一个已设置的环境变量值;都未设置则返回 default。
|
|
249
|
+
|
|
250
|
+
用于兼容占位符风格与显式 override 风格两套环境变量命名。
|
|
251
|
+
例如 ``discovery.server_addr`` 同时支持 ``NACOS_SERVER``(占位符风格)
|
|
252
|
+
和 ``DISCOVERY_SERVER_ADDR``(显式覆盖风格)。
|
|
253
|
+
"""
|
|
254
|
+
for name in names:
|
|
255
|
+
value = os.getenv(name)
|
|
256
|
+
if value is not None:
|
|
257
|
+
return value
|
|
258
|
+
return default
|
|
259
|
+
|
|
260
|
+
@staticmethod
|
|
261
|
+
def _get_env_int(name: str, default: int) -> int:
|
|
262
|
+
"""读取整型环境变量,转换失败抛 ``ConfigurationError`` 提示友好错误。"""
|
|
263
|
+
raw = os.getenv(name)
|
|
264
|
+
if raw is None:
|
|
265
|
+
return default
|
|
266
|
+
try:
|
|
267
|
+
return int(raw)
|
|
268
|
+
except (ValueError, TypeError):
|
|
269
|
+
raise ConfigurationError(f"环境变量 {name} 必须是整数,实际值: {raw!r}")
|
|
270
|
+
|
|
132
271
|
def _override_with_env(self):
|
|
133
272
|
"""使用环境变量覆盖配置"""
|
|
134
273
|
self._config.setdefault('spring', {})
|
|
@@ -142,7 +281,7 @@ class ConfigLoader:
|
|
|
142
281
|
fail_fast_env = os.getenv('STARTUP_FAIL_FAST')
|
|
143
282
|
configured_fail_fast = self._config['startup'].get('fail_fast')
|
|
144
283
|
if fail_fast_env is not None:
|
|
145
|
-
self._config['startup']['fail_fast'] = fail_fast_env
|
|
284
|
+
self._config['startup']['fail_fast'] = _to_bool(fail_fast_env)
|
|
146
285
|
elif configured_fail_fast is None:
|
|
147
286
|
self._config['startup'].pop('fail_fast', None)
|
|
148
287
|
else:
|
|
@@ -151,71 +290,96 @@ class ConfigLoader:
|
|
|
151
290
|
# Redis配置
|
|
152
291
|
self._config.setdefault('redis', {})
|
|
153
292
|
self._config['redis']['host'] = os.getenv('REDIS_HOST', self._config['redis'].get('host', 'localhost'))
|
|
154
|
-
self._config['redis']['port'] =
|
|
155
|
-
self._config['redis']['db'] =
|
|
293
|
+
self._config['redis']['port'] = self._get_env_int('REDIS_PORT', self._config['redis'].get('port', 6379))
|
|
294
|
+
self._config['redis']['db'] = self._get_env_int('REDIS_DB', self._config['redis'].get('db', 0))
|
|
156
295
|
self._config['redis']['password'] = os.getenv('REDIS_PASSWORD', self._config['redis'].get('password'))
|
|
157
|
-
self._config['redis']['enabled'] =
|
|
158
|
-
|
|
296
|
+
self._config['redis']['enabled'] = _to_bool(
|
|
297
|
+
os.getenv('REDIS_ENABLED', self._config['redis'].get('enabled', False)), False)
|
|
298
|
+
|
|
159
299
|
# JWT配置
|
|
160
300
|
self._config.setdefault('jwt', {})
|
|
161
301
|
self._config['jwt']['secret_key'] = os.getenv('JWT_SECRET_KEY', self._config['jwt'].get('secret_key', 'spring-python-secret-key-change-in-production'))
|
|
162
302
|
self._config['jwt']['algorithm'] = os.getenv('JWT_ALGORITHM', self._config['jwt'].get('algorithm', 'HS256'))
|
|
163
|
-
|
|
303
|
+
|
|
164
304
|
# 数据库配置
|
|
165
305
|
self._config.setdefault('database', {})
|
|
166
306
|
self._config['database']['url'] = os.getenv('DB_URL', self._config['database'].get('url', 'sqlite:///./test.db'))
|
|
167
|
-
self._config['database']['echo'] =
|
|
168
|
-
|
|
307
|
+
self._config['database']['echo'] = _to_bool(
|
|
308
|
+
os.getenv('DB_ECHO', self._config['database'].get('echo', False)), False)
|
|
309
|
+
# database.enabled 默认 True(对齐 application.yml 占位符 ${DB_ENABLED:true})
|
|
310
|
+
self._config['database']['enabled'] = _to_bool(
|
|
311
|
+
os.getenv('DB_ENABLED', self._config['database'].get('enabled', True)), True)
|
|
169
312
|
# PyMyBatis原生数据源配置(host/port/driver等)
|
|
170
313
|
self._config['database']['driver'] = os.getenv('DB_DRIVER', self._config['database'].get('driver', 'sqlite'))
|
|
171
314
|
self._config['database']['host'] = os.getenv('DB_HOST', self._config['database'].get('host', 'localhost'))
|
|
172
|
-
self._config['database']['port'] =
|
|
315
|
+
self._config['database']['port'] = self._get_env_int('DB_PORT', self._config['database'].get('port', 3306))
|
|
173
316
|
self._config['database']['database'] = os.getenv('DB_NAME', self._config['database'].get('database', 'test'))
|
|
174
317
|
self._config['database']['username'] = os.getenv('DB_USERNAME', self._config['database'].get('username', ''))
|
|
175
318
|
self._config['database']['password'] = os.getenv('DB_PASSWORD', self._config['database'].get('password', ''))
|
|
176
|
-
|
|
319
|
+
|
|
177
320
|
# 服务发现配置
|
|
321
|
+
# 兼容占位符风格(NACOS_*)与显式覆盖风格(DISCOVERY_*)两套环境变量命名
|
|
178
322
|
self._config.setdefault('discovery', {})
|
|
179
|
-
self._config['discovery']['server_addr'] =
|
|
180
|
-
|
|
181
|
-
|
|
323
|
+
self._config['discovery']['server_addr'] = self._get_env_any(
|
|
324
|
+
'NACOS_SERVER', 'DISCOVERY_SERVER_ADDR',
|
|
325
|
+
default=self._config['discovery'].get('server_addr', 'localhost:8848'))
|
|
326
|
+
self._config['discovery']['namespace'] = self._get_env_any(
|
|
327
|
+
'NACOS_NAMESPACE', 'DISCOVERY_NAMESPACE',
|
|
328
|
+
default=self._config['discovery'].get('namespace', ''))
|
|
329
|
+
self._config['discovery']['group'] = self._get_env_any(
|
|
330
|
+
'NACOS_GROUP', 'DISCOVERY_GROUP',
|
|
331
|
+
default=self._config['discovery'].get('group', 'DEFAULT_GROUP'))
|
|
182
332
|
self._config['discovery']['username'] = os.getenv('NACOS_USERNAME', self._config['discovery'].get('username', ''))
|
|
183
333
|
self._config['discovery']['password'] = os.getenv('NACOS_PASSWORD', self._config['discovery'].get('password', ''))
|
|
184
|
-
self._config['discovery']['enabled'] =
|
|
185
|
-
|
|
334
|
+
self._config['discovery']['enabled'] = _to_bool(
|
|
335
|
+
os.getenv('DISCOVERY_ENABLED', self._config['discovery'].get('enabled', False)), False)
|
|
336
|
+
|
|
186
337
|
# Seata配置
|
|
338
|
+
# 兼容占位符风格(SEATA_SERVER/SEATA_APP_ID/SEATA_TX_GROUP)与显式覆盖风格
|
|
187
339
|
self._config.setdefault('seata', {})
|
|
188
|
-
self._config['seata']['server_addr'] =
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
self._config['seata']['
|
|
192
|
-
|
|
340
|
+
self._config['seata']['server_addr'] = self._get_env_any(
|
|
341
|
+
'SEATA_SERVER', 'SEATA_SERVER_ADDR',
|
|
342
|
+
default=self._config['seata'].get('server_addr', 'localhost:8091'))
|
|
343
|
+
self._config['seata']['application_id'] = self._get_env_any(
|
|
344
|
+
'SEATA_APP_ID', 'SEATA_APPLICATION_ID',
|
|
345
|
+
default=self._config['seata'].get('application_id', ''))
|
|
346
|
+
self._config['seata']['transaction_group'] = self._get_env_any(
|
|
347
|
+
'SEATA_TX_GROUP', 'SEATA_TRANSACTION_GROUP',
|
|
348
|
+
default=self._config['seata'].get('transaction_group', 'my_tx_group'))
|
|
349
|
+
self._config['seata']['enabled'] = _to_bool(
|
|
350
|
+
os.getenv('SEATA_ENABLED', self._config['seata'].get('enabled', False)), False)
|
|
351
|
+
|
|
193
352
|
# RabbitMQ配置
|
|
353
|
+
# 兼容占位符风格(RABBITMQ_VHOST)与显式覆盖风格(RABBITMQ_VIRTUAL_HOST)
|
|
194
354
|
self._config.setdefault('rabbitmq', {})
|
|
195
355
|
self._config['rabbitmq']['host'] = os.getenv('RABBITMQ_HOST', self._config['rabbitmq'].get('host', 'localhost'))
|
|
196
|
-
self._config['rabbitmq']['port'] =
|
|
356
|
+
self._config['rabbitmq']['port'] = self._get_env_int('RABBITMQ_PORT', self._config['rabbitmq'].get('port', 5672))
|
|
197
357
|
self._config['rabbitmq']['username'] = os.getenv('RABBITMQ_USERNAME', self._config['rabbitmq'].get('username', 'guest'))
|
|
198
358
|
self._config['rabbitmq']['password'] = os.getenv('RABBITMQ_PASSWORD', self._config['rabbitmq'].get('password', 'guest'))
|
|
199
|
-
self._config['rabbitmq']['virtual_host'] =
|
|
200
|
-
|
|
201
|
-
|
|
359
|
+
self._config['rabbitmq']['virtual_host'] = self._get_env_any(
|
|
360
|
+
'RABBITMQ_VHOST', 'RABBITMQ_VIRTUAL_HOST',
|
|
361
|
+
default=self._config['rabbitmq'].get('virtual_host', '/'))
|
|
362
|
+
self._config['rabbitmq']['enabled'] = _to_bool(
|
|
363
|
+
os.getenv('RABBITMQ_ENABLED', self._config['rabbitmq'].get('enabled', False)), False)
|
|
364
|
+
|
|
202
365
|
# Prometheus配置
|
|
203
366
|
self._config.setdefault('prometheus', {})
|
|
204
367
|
self._config['prometheus']['namespace'] = os.getenv('PROMETHEUS_NAMESPACE', self._config['prometheus'].get('namespace', 'spring'))
|
|
205
368
|
self._config['prometheus']['subsystem'] = os.getenv('PROMETHEUS_SUBSYSTEM', self._config['prometheus'].get('subsystem', 'python'))
|
|
206
|
-
self._config['prometheus']['port'] =
|
|
207
|
-
self._config['prometheus']['enabled'] =
|
|
208
|
-
|
|
369
|
+
self._config['prometheus']['port'] = self._get_env_int('PROMETHEUS_PORT', self._config['prometheus'].get('port', 8000))
|
|
370
|
+
self._config['prometheus']['enabled'] = _to_bool(
|
|
371
|
+
os.getenv('PROMETHEUS_ENABLED', self._config['prometheus'].get('enabled', False)), False)
|
|
372
|
+
|
|
209
373
|
# 日志配置
|
|
210
374
|
self._config.setdefault('logging', {})
|
|
211
375
|
self._config['logging']['level'] = os.getenv('LOG_LEVEL', self._config['logging'].get('level', 'INFO'))
|
|
212
376
|
self._config['logging']['log_dir'] = os.getenv('LOG_DIR', self._config['logging'].get('log_dir', 'logs'))
|
|
213
377
|
self._config['logging']['retention'] = os.getenv('LOG_RETENTION', self._config['logging'].get('retention', '30 days'))
|
|
214
378
|
self._config['logging']['rotation'] = os.getenv('LOG_ROTATION', self._config['logging'].get('rotation', '100 MB'))
|
|
215
|
-
|
|
379
|
+
|
|
216
380
|
# 服务器配置
|
|
217
381
|
self._config.setdefault('server', {})
|
|
218
|
-
self._config['server']['port'] =
|
|
382
|
+
self._config['server']['port'] = self._get_env_int('SERVER_PORT', self._config['server'].get('port', 8080))
|
|
219
383
|
self._config['server']['host'] = os.getenv('SERVER_HOST', self._config['server'].get('host', '0.0.0.0'))
|
|
220
384
|
|
|
221
385
|
self._config['server'].setdefault('cors', {})
|
|
@@ -227,9 +391,52 @@ class ConfigLoader:
|
|
|
227
391
|
]
|
|
228
392
|
else:
|
|
229
393
|
cors_config.setdefault('allow_origins', [])
|
|
230
|
-
cors_config['allow_credentials'] =
|
|
231
|
-
'CORS_ALLOW_CREDENTIALS',
|
|
232
|
-
|
|
394
|
+
cors_config['allow_credentials'] = _to_bool(
|
|
395
|
+
os.getenv('CORS_ALLOW_CREDENTIALS', cors_config.get('allow_credentials', False)), False)
|
|
396
|
+
|
|
397
|
+
def _override_with_cli_args(self) -> None:
|
|
398
|
+
"""从命令行参数覆盖配置(优先级最高,对齐 Spring Boot ``--key=value``)。
|
|
399
|
+
|
|
400
|
+
支持两种形式:
|
|
401
|
+
- ``--server.port=9000``
|
|
402
|
+
- ``--server.port 9000``
|
|
403
|
+
|
|
404
|
+
点分隔键递归写入 ``self._config``,值经 ``yaml.safe_load`` 做类型推断
|
|
405
|
+
(``9000`` → int,``true`` → bool)。在 ``_override_with_env`` 之后调用。
|
|
406
|
+
"""
|
|
407
|
+
args = sys.argv[1:]
|
|
408
|
+
i = 0
|
|
409
|
+
while i < len(args):
|
|
410
|
+
arg = args[i]
|
|
411
|
+
if not arg.startswith('--'):
|
|
412
|
+
i += 1
|
|
413
|
+
continue
|
|
414
|
+
body = arg[2:]
|
|
415
|
+
if '=' in body:
|
|
416
|
+
key, value = body.split('=', 1)
|
|
417
|
+
self._set_cli_override(key, value)
|
|
418
|
+
i += 1
|
|
419
|
+
else:
|
|
420
|
+
# --key value 形式:仅当下一个 token 非参数时才取值
|
|
421
|
+
if i + 1 < len(args) and not args[i + 1].startswith('--'):
|
|
422
|
+
self._set_cli_override(body, args[i + 1])
|
|
423
|
+
i += 2
|
|
424
|
+
else:
|
|
425
|
+
i += 1
|
|
426
|
+
|
|
427
|
+
def _set_cli_override(self, dotted_key: str, raw_value: str) -> None:
|
|
428
|
+
"""将点分隔键写入 ``self._config``,值做 yaml 类型推断。"""
|
|
429
|
+
keys = dotted_key.split('.')
|
|
430
|
+
node = self._config
|
|
431
|
+
for k in keys[:-1]:
|
|
432
|
+
child = node.get(k)
|
|
433
|
+
if not isinstance(child, dict):
|
|
434
|
+
child = {}
|
|
435
|
+
node[k] = child
|
|
436
|
+
node = child
|
|
437
|
+
parsed = yaml.safe_load(raw_value)
|
|
438
|
+
node[keys[-1]] = parsed
|
|
439
|
+
logger.debug(f"CLI override: {dotted_key}={parsed!r}")
|
|
233
440
|
|
|
234
441
|
def _validate_config(self) -> None:
|
|
235
442
|
algorithm = str(self._config.get('jwt', {}).get('algorithm', 'HS256')).upper()
|
|
@@ -336,26 +543,46 @@ class ConfigLoader:
|
|
|
336
543
|
"""
|
|
337
544
|
return self._config.get('spring', {}).get('profiles', {}).get('active', 'default')
|
|
338
545
|
|
|
546
|
+
@staticmethod
|
|
547
|
+
def _lookup_key(mapping: Any, key: str):
|
|
548
|
+
"""在字典中查找 ``key``:先精确匹配,再松散匹配(大小写/分隔符不敏感)。
|
|
549
|
+
|
|
550
|
+
与 ``@ConfigurationProperties`` 松散绑定语义一致,使 ``get('logging.log-dir')``
|
|
551
|
+
也能命中 YAML 中的 ``log_dir``。返回 ``(value, found)``。
|
|
552
|
+
"""
|
|
553
|
+
if not isinstance(mapping, dict):
|
|
554
|
+
return None, False
|
|
555
|
+
if key in mapping:
|
|
556
|
+
return mapping[key], True
|
|
557
|
+
# 松散匹配:规范化(去分隔符+小写)后比较
|
|
558
|
+
norm = _normalize_name(key)
|
|
559
|
+
for k, v in mapping.items():
|
|
560
|
+
if _normalize_name(k) == norm:
|
|
561
|
+
return v, True
|
|
562
|
+
return None, False
|
|
563
|
+
|
|
339
564
|
def get(self, key: str, default: Any = None) -> Any:
|
|
340
565
|
"""
|
|
341
566
|
获取配置值
|
|
342
|
-
|
|
567
|
+
|
|
568
|
+
支持点分隔路径与松散绑定(kebab-case / snake_case / 大小写不敏感)。
|
|
569
|
+
先精确匹配,未命中再松散匹配,避免 ``log-dir`` 找不到 ``log_dir`` 的问题。
|
|
570
|
+
|
|
343
571
|
Args:
|
|
344
|
-
key: 配置键,支持点分隔(如 redis.host)
|
|
572
|
+
key: 配置键,支持点分隔(如 redis.host / logging.log-dir / Logging.Level)
|
|
345
573
|
default: 默认值
|
|
346
|
-
|
|
574
|
+
|
|
347
575
|
Returns:
|
|
348
576
|
配置值
|
|
349
577
|
"""
|
|
350
578
|
keys = key.split('.')
|
|
351
579
|
value = self._config
|
|
352
|
-
|
|
580
|
+
|
|
353
581
|
for k in keys:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
else:
|
|
582
|
+
value, found = self._lookup_key(value, k)
|
|
583
|
+
if not found:
|
|
357
584
|
return default
|
|
358
|
-
|
|
585
|
+
|
|
359
586
|
return value
|
|
360
587
|
|
|
361
588
|
def load_config(self):
|
|
@@ -421,6 +421,14 @@ class ApplicationContext:
|
|
|
421
421
|
# 处理字段上的@Value注解
|
|
422
422
|
for name, field in inspect.getmembers(bean_class):
|
|
423
423
|
if not name.startswith('_'):
|
|
424
|
+
# @Value/@NacosValue can be attached to a function as
|
|
425
|
+
# metadata alongside route/AOP annotations. Replacing
|
|
426
|
+
# that function on the instance makes the handler
|
|
427
|
+
# non-callable and breaks controller registration.
|
|
428
|
+
# Constructor/default and class-field injection are
|
|
429
|
+
# handled separately by BeanFactory.
|
|
430
|
+
if callable(field):
|
|
431
|
+
continue
|
|
424
432
|
annotations = getattr(field, '__spring_annotations__', [])
|
|
425
433
|
for annotation in annotations:
|
|
426
434
|
if isinstance(annotation, Value):
|